Evert Pot's blog2025-03-03T05:43:51+00:00http://evertpot.com/Evert Pot[email protected]http://evertpot.com/letting-curveball-die/Putting Curveball in maintenance mode2024-12-29T15:00:00+00:00Evert Pot[email protected]<p><a href="https://curveballjs.org/">Curveball</a> is a server-side webframework for Node.js. I’ve been working on
it for 6 years as a side-project, but it wasn’t successful and it’s time to let
it go. This is a hard thing to do, because I probably have 1000’s of hours into
this project, and there’s a strong sense of missed potential.</p>
<p>I’m writing this article because my psyche requires closure in order for them to
not take up space in my mind. This decision was probably a few years late.
Hopefully this creates more space for new things.</p>
<h2 id="some-quick-numbers">Some quick numbers.</h2>
<ul>
<li>Number of <a href="https://www.npmjs.com/search?q=%40curveball">NPM packages</a>: <strong>25</strong></li>
<li>Number of released packages: <strong>377</strong></li>
<li>Total number of commits: <strong>4882</strong></li>
<li>Contributers: <strong>32</strong></li>
<li>Total downloads from NPM: <strong>1,254,023</strong></li>
<li>Active users: <strong>10?</strong></li>
</ul>
<h2 id="why-build-another-framework">Why build another framework</h2>
<p>In 2018, Typescript just really started popping off and at the time I was
deeply interested in optimizing hypermedia-centric REST APIs, <a href="https://koajs.com/">Koa</a> was
a favourite framework of mine, but it was missing some features I was
particularly interested in.</p>
<ul>
<li>Good Typescript support (This is pretty much fixed in Koa, but at the time
it was buggy).</li>
<li>The ability to do internal “sub-requests” without going over the network.
(think: hitting a different route in your framework with a local function).
Koa (and express) wrap the <code class="language-plaintext highlighter-rouge">http</code> Node.js library, and don’t have a good
internal abstraction for requests and responses. This makes sub-requests
hard and deploying into non-node runtimes such as AWS Lambdas very painful
and requires mocking the whole Node <code class="language-plaintext highlighter-rouge">http</code> library or worse: run full HTTP
server inside the lambda function just so requests can be forwarded.
New frameworks like <a href="https://hono.dev/">Hono</a> do this perfectly these days, but this wasn’t
around at the time.</li>
<li>Native <a href="https://evertpot.com/jsx-template/">‘JSX as a template engine’</a> support. <a href="https://hono.dev/docs/guides/jsx">Hono</a> also does this.</li>
<li>Deeply integrated Websockets. Probably <a href="https://bun.sh/docs/api/websockets">Bun</a> does this best at the
moment.</li>
<li>Support HTTP/2. A big motivator for me was being able to push resources
down the pipe with push, and <a href="https://evertpot.com/h2-parallelism/">solve the N+1 problem for REST</a>. <a href="https://www.npmjs.com/package/@curveball/http-errors">Push is
pretty much dead</a> now, so less of a priority.</li>
<li>Good support for <code class="language-plaintext highlighter-rouge">103 Early Hints</code>. Not seeing a lot love for this in the
new batch of frameworks.</li>
</ul>
<h2 id="why-did-curveball-fail">Why did Curveball fail</h2>
<p>If I’m being generous with myself, I think the tech was decent and generally
liked by those using it. It was fairly sticky, but it never really reached
anything close to critical mass.</p>
<p>Documentation was bad, and crucially marketing was pretty much non-existant.
Over the entire 6 years it was mostly me working on it (with some great smaller
contributions!), but I have a very hard time with marketing and speaking about
the project with some consistency. The last time I blogged about a release was
in 2020. It never had a chance.</p>
<p>I made the same mistake I made many times before, thinking ‘If I build it, they
will come’ and I guess I had a hope it would attract some early strong
contributors that could help pick up the slack in areas where I was weak, but
obviously this never happened.</p>
<p>I also don’t see how I could have gotten the time and motivation to promote
the project. I find self-promotion somewhat painful to do and Curveball was
always at best <em>just</em> incrementally better than what was out there when I
started it, so not a lot of people are going to take the chance on something
with low user numbers, even if they were able to find it.</p>
<p>I’m still interested in Open Source and love building things, but probably the
biggest lesson here is that if I want it to be successful I need to do group
projects with people who make up for the qualities I lack.</p>
<h2 id="im-using-curveball-for-my-project-now-what">I’m using Curveball for my project, now what?</h2>
<p>The project has been very stable and only had 1 major breaking change, which
was the move to ESM.</p>
<p>Express has gotten by without a major release in over a decade, so if you’re
using Curveball today I think there’s very little chance you need to do
anything in a long while. I will even fix bugs as they are reported, but this
blog post is effectively my statement that I’m no longer going to put much
energy in it. Curveball is stable. Don’t use it for new projects.</p>
<p>If you need advice on a migration path, I’m happy to do a free 30 minute
consultation! You probably know how to reach me.</p>
<h2 id="if-you-like-the-project-why-cant-you-work-on-it-despite-not-having-many-users">If you like the project, why can’t you work on it despite not having many users</h2>
<p>I get a lot of energy from working with people, getting feedback and seeing
my ideas validated. If this is not happening, to me it feels a bit like writing
a book that nobody reads. It’s a constant reminder of failure.</p>
<h2 id="are-there-any-bits-salvagable">Are there any bits salvagable?</h2>
<ul>
<li>I’m going to rename <a href="https://www.npmjs.com/package/@curveball/http-errors">@curveball/http-errors</a> because it sucks less than
some other HTTP error packages, it’s not tied to the framework and is
generally useful.</li>
<li>I think the built-in <a href="https://github.com/curveball/browser/">HATEOAS API browser</a> is great and I’m going to
port it to a new framework. (most likely Hono as it’s the closest in spirit).</li>
<li><a href="https://github.com/curveball/a12n-server">a12n-server</a>, which is an authn/authz server similar to keycloak is
still very interesting to me. It needs its own brand and logo and will port
this project to a different framework at some future date. Despite being
under the ‘curveball’ namespace I always saw this as its own project. This
project does suffer from a similar problem though. Low uptake and just me
maintaining it. I need to set some goals for 2025 and kill this as well if
I don’t meet them.</li>
</ul>
<h2 id="thank-you">Thank you</h2>
<p>If you’re a current user of Curveball: thank you for giving this a shot! I
hope this news is not too frustrating.</p>
<p>To my friends that gave me encouragement and feedback over the years: I love
you and appreciate you!</p>
http://evertpot.com/using-top-level-await-is-bc-break/In the future using top-level await might be cause a backwards compatibility break in Node2024-10-18T01:13:00+00:00Evert Pot[email protected]<p><a href="https://nodejs.org/en/blog/release/v23.0.0">Node 23</a> was released this week, and the hot ticket item probably is the
fact that you can now <code class="language-plaintext highlighter-rouge">require()</code> files that use ESM (<code class="language-plaintext highlighter-rouge">import</code>/<code class="language-plaintext highlighter-rouge">export</code>).</p>
<p>This is helpful because ESM and CommonJS (<code class="language-plaintext highlighter-rouge">require</code>/<code class="language-plaintext highlighter-rouge">module.exports</code>) are kind
of different worlds and before this change if you wanted to use a “module”
from your CommonJS file, you would need to do something like:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">theThing</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">some/module/file.mjs</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>
<p>This is called a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import">dynamic import</a> and it’s extra annoying, because you
can’t just put this at the top of your file. It returns a Promise, so you
can only import these modules ‘asynchronously’ or in functions. The reason
is that you can only <code class="language-plaintext highlighter-rouge">await</code> inside functions in CommonJS files. So this
syntax prevents importing a “module” and immediately using it. To use a
module in CommonJS, you’ll need some kind of initialization logic.</p>
<p>Many Javascript packages resort to <a href="https://evertpot.com/universal-commonjs-esm-typescript-packages/">shipping both a ‘CommonJS’ and a ‘ESM’
version of their code</a> to reduce this kind of annoyance, but not everyone
does.</p>
<h2 id="the-node-23-change">The Node 23 change</h2>
<p>Node 23 makes it possible to load ESM modules transparently via <code class="language-plaintext highlighter-rouge">require()</code>.
This means that the above example can be rewritten to:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">theThing</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">some/module/file.mjs</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>
<p>The important part here is not the usage of <code class="language-plaintext highlighter-rouge">require</code>, but the absense of
<code class="language-plaintext highlighter-rouge">await</code>. This allows ESM modules to be loaded without this kind of
initialization logic.</p>
<p>But there’s a big caveat:</p>
<p>This <em>only</em> works if the module you loaded in is not using top-level await.
“Top level await” means awaiting things outside of (async) functions, which
is possible in modules (but not CommonJS).</p>
<p>If a top-level await <em>was</em> used, a <code class="language-plaintext highlighter-rouge">ERR_REQUIRE_ASYNC_MODULE</code> error will be
thrown. But the important thing to note is that this doesn’t just apply to
the file you are directly importing/requiring. It also applies to any files
loaded by that file, at any level in either your project or any dependency or
sub-dependencies.</p>
<h2 id="using-top-level-await-is-now-a-bc-break">Using top-level await is now a BC break</h2>
<p>Typically when we think of backwards compatibility breaks that require a major
new version in semver, you might think of functions changing, or removing or
arguments no longer being supported.</p>
<p>Before this change in Node, if your project was fully switched to ESM you might
not think of placing a top-level <code class="language-plaintext highlighter-rouge">await</code> anywhere in your code as a backwards
compatibility break, but if it’s the first <code class="language-plaintext highlighter-rouge">await</code> you might now inadvertently
break Node.js users, if they used <code class="language-plaintext highlighter-rouge">require()</code> to bring in your module.</p>
<p>This means that the first top-level <code class="language-plaintext highlighter-rouge">await</code> in your project (or any of your
dependencies) might now constitute a new major version if you follow <a href="https://semver.org/">semver</a>.</p>
<p>If you don’t want to do this, here are some other things you could do:</p>
<h3 id="1-tell-your-users-you-dont-support-require">1. Tell your users you don’t support require()</h3>
<p>You could effectively tell them if they use <code class="language-plaintext highlighter-rouge">require()</code> they’re on their own.
Chances are that your users won’t read this and do it anyway, but arguably they
should read the readme of a package they bring in.</p>
<h3 id="2-add-a-dummy-await">2. Add a dummy await</h3>
<p>Do you think you or your dependencies might use a top-level await in the
future, but you don’t yet? You could add this line to your source to reserve
the right to do it later:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="dl">"</span><span class="s2">Good things come to those that support await</span><span class="dl">"</span>
</code></pre></div></div>
<h3 id="3-explictly-break-commonjs">3. Explictly break CommonJS</h3>
<p>Packages can provide both CommonJS and ESM support via the <a href="https://nodejs.org/api/packages.html#exports"><code class="language-plaintext highlighter-rouge">exports</code></a> key
in <code class="language-plaintext highlighter-rouge">package.json</code>. It could be an option to export a single CommonJS file that
just throws an error to warn CommonJS users they shouldn’t use this package.</p>
<p>Out of these 3 options, I kind of like 2 because if Node.js ever changes
behavior and <em>does</em> support loading modules that use top-level await in
CommonJS files it will one day just start working.</p>
<p>Regardless. it does feel important to me that package maintainers that only
ship ESM, or ship CommonJS and intend to use this new feature have a
strategy, because it’s bound to blow up if not.</p>
<h2 id="how-do-other-runtimes-do-it">How do other runtimes do it?</h2>
<p>In Bun you can use <a href="https://docs.deno.com/runtime/tutorials/cjs_to_esm/"><code class="language-plaintext highlighter-rouge">import</code> and <code class="language-plaintext highlighter-rouge">require</code></a> in the same file, so there’s
fewer trade-offs to make. But if you do use <code class="language-plaintext highlighter-rouge">require()</code> to import a module
with top-level await it still has the same issue. There’s just fewer reasons
in Bun to even want to do this.</p>
<p>Deno <a href="https://docs.deno.com/runtime/tutorials/cjs_to_esm/">doesn’t support CommonJS</a> so it doesn’t have that problem.</p>
<h2 id="does-the-issue-exists-when-importing-commonjs-files-into-modules">Does the issue exists when importing CommonJS files into modules?</h2>
<p>Importing CommonJS files into modules was always possible via the standard
<code class="language-plaintext highlighter-rouge">import</code> syntax, and because CommonJS can’t asynchronously export this
problem doesn’t exist there.</p>
<h2 id="what-should-i-do-as-a-user">What should I do as a user?</h2>
<p>Most of this article was about the effects for package maintainers, but not every
package will do this well so as a user you’re still potentially exposed to this
issue.</p>
<p>In order of preference, consider the following:</p>
<ol>
<li>Stop using CommonJS. Start using ESM everywhere. This issue only exists in
CommonJS files using ESM, not the reverse. ESM is the future. You’re working
with dead-end technology.</li>
<li>Have some testing in place that at the very least loads the entirely of your
code-base. If all (top-level, not-dynamic) <code class="language-plaintext highlighter-rouge">requires()</code> are ran/resolved,
any changes in depndencies should just blow up your CI environment and
application.</li>
<li>Find out if any of your dependencies are ESM-only, and never <code class="language-plaintext highlighter-rouge">require()</code>
them. This protects you from issues with direct dependencies. But keep
in mind that this can still be an issue in sub-dependencies even if your
direct dependencies are CommonJS themselves!</li>
</ol>
<h2 id="my-opinion">My opinion</h2>
<p>For the above reasons I don’t think this should land in a stable Node.js
version. The intentions are good but it creates yet another avenue of confusion.</p>
<p>It effectively expands the number of Javascript flavors from 2 to 3:</p>
<ol>
<li>CommonJS</li>
<li>ESM</li>
<li>ESM but without top-level await.</li>
</ol>
<p>If a module uses flavor #3, it’s compatible with flavor #1, but if anywhere in
the dependency tree something starts using top-level await, suddenly the entire
tree ESM dependency tree cascades from flavor #3 to #2 and breaks compatibilty
with flavor #1.</p>
<p>Given the already confusing landscape (and reputation) of Javascript modules,
this feels like a step in the wrong direction, as it adds a feature to
CommonJS (which in my opinion is time to freeze), and makes ESM <em>less</em>
reliable as a result.</p>
http://evertpot.com/discovering-features-with-http-options/Discovering features using HTTP OPTIONS2024-10-16T13:44:00+00:00Evert Pot[email protected]<p>Say you have an API, and you want to communicate what sort of things a user can
do on a specific endpoint. You can use external description formats like OpenAPI
or JSON Schema, but sometimes it’s nice to also dynamically communicate this on
the API itself.</p>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS"><code class="language-plaintext highlighter-rouge">OPTIONS</code></a> is the method used for that. You may know this HTTP method from
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS">CORS</a>, but it’s general purpose is for clients to passively find out ‘What
can I do here?’.</p>
<p>All HTTP clients typically support making <code class="language-plaintext highlighter-rouge">OPTIONS</code> request. For example with
<code class="language-plaintext highlighter-rouge">fetch()</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span>
<span class="dl">'</span><span class="s1">https://example.org</span><span class="dl">'</span><span class="p">,</span>
<span class="p">{</span><span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">OPTIONS</span><span class="dl">'</span><span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<p>A basic <code class="language-plaintext highlighter-rouge">OPTIONS</code> response might might look like this:</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">204</span> <span class="ne">No Content</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Mon, 23 Sep 2024 02:57:38 GMT</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">KKachel/1.2</span>
<span class="na">Allow</span><span class="p">:</span> <span class="s">GET, PUT, POST, DELETE, OPTIONS</span>
</code></pre></div></div>
<p>Based on the <a href="https://www.rfc-editor.org/rfc/rfc9110.html#field.allow" title="Allow header"><code class="language-plaintext highlighter-rouge">Allow</code></a> header you can quickly tell which HTTP methods
are available at a given endpoint. Many web frameworks emit this automatically
and generate the list of methods dynamically per route, so chances are that you
get this one for free.</p>
<p>To find out if your server does, try running the command below (with your
URL!):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -X OPTIONS http://localhost:3000/some/endpoint/
</code></pre></div></div>
<p>One nice thing you could do with the <code class="language-plaintext highlighter-rouge">Allow</code> header, is that you could also
communicate access-control information on a very basic level. For example,
you could only include <code class="language-plaintext highlighter-rouge">DELETE</code> and <code class="language-plaintext highlighter-rouge">PUT</code> if a user has write access to
a resource.</p>
<h2 id="accept-and-accept-encoding">Accept and Accept-Encoding</h2>
<p>There’s server other standard headers for discovery. Here’s an example showing
a few at once:</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">204</span> <span class="ne">No Content</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Mon, 23 Sep 2024 02:57:38 GMT</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">KKachel/1.2</span>
<span class="na">Allow</span><span class="p">:</span> <span class="s">GET, PUT, POST, DELETE, OPTIONS</span>
<span class="na">Accept</span><span class="p">:</span> <span class="s">application/vnd.my-company-api+json, application/json, text/html</span>
<span class="na">Accept-Encoding</span><span class="p">:</span> <span class="s">gzip,brotli,identity</span>
</code></pre></div></div>
<p>You may already be familiar with <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-accept" title="Accept header"><code class="language-plaintext highlighter-rouge">Accept</code></a> and <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-encoding" title="Accept-Encoding header"><code class="language-plaintext highlighter-rouge">Accept-Encoding</code></a> from
HTTP requests, but they can also appear in responses. <code class="language-plaintext highlighter-rouge">Accept</code> in a response
lets you tell the client which kind of mimetypes are available at an endpoint.
I like adding <code class="language-plaintext highlighter-rouge">text/html</code> to every JSON api endpoint and making sure that
API urls can be opened in browsers and shared between devs for easy debugging.</p>
<p>The <code class="language-plaintext highlighter-rouge">Accept-Encoding</code> lets a client know in this case that they can compress
their request bodies with either <code class="language-plaintext highlighter-rouge">gzip</code> or <code class="language-plaintext highlighter-rouge">brotli</code> (<code class="language-plaintext highlighter-rouge">identity</code> means no
compression).</p>
<h2 id="patching-posting-and-querying">Patching, posting and querying</h2>
<p>3 other headers that can be used are <a href="https://www.rfc-editor.org/rfc/rfc5789#section-3.1" title="Accept-Patch header"><code class="language-plaintext highlighter-rouge">Accept-Patch</code></a>, <a href="https://www.w3.org/TR/ldp/#header-accept-post" title="Accept-Post header"><code class="language-plaintext highlighter-rouge">Accept-Post</code></a>
and <a href="https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-safe-method-w-body-05#name-the-accept-query-header-fie" title="Accept-Query header"><code class="language-plaintext highlighter-rouge">Accept-Query</code></a>. These three headers are used to tell a client what
content-types are available for the <a href="https://www.rfc-editor.org/rfc/rfc5789#section-2" title="PATCH method"><code class="language-plaintext highlighter-rouge">PATCH</code></a>, <a href="https://www.rfc-editor.org/rfc/rfc9110.html#name-post" title="POST method"><code class="language-plaintext highlighter-rouge">POST</code></a> and
<a href="https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-safe-method-w-body-05#name-query" title="QUERY method"><code class="language-plaintext highlighter-rouge">QUERY</code></a> http methods respectively.</p>
<p>For all of these headers, their values effectively dictate what valid
values are for the <code class="language-plaintext highlighter-rouge">Content-Type</code> header when making the request.</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">204</span> <span class="ne">No Content</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Mon, 23 Sep 2024 02:57:38 GMT</span>
<span class="na">Server</span><span class="p">:</span> <span class="s">KKachel/1.2</span>
<span class="na">Allow</span><span class="p">:</span> <span class="s">OPTIONS, QUERY, POST, PATCH</span>
<span class="na">Accept-Patch</span><span class="p">:</span> <span class="s">application/json-patch+json, application/merge-patch+json</span>
<span class="na">Accept-Query</span><span class="p">:</span> <span class="s">application/graphql</span>
<span class="na">Accept-Post</span><span class="p">:</span> <span class="s">multipart/form-data, application/vnd.custom.rpc+json</span>
</code></pre></div></div>
<p>In the above response, the server indicates it supports both <a href="https://datatracker.ietf.org/doc/html/rfc6902" title="JSON Patch">JSON Patch</a>
and <a href="https://datatracker.ietf.org/doc/html/rfc7386" title="JSON Merge Patch">JSON Merge Patch</a> content-types in <code class="language-plaintext highlighter-rouge">PATCH</code> requests. It also suggests
that GraphQL can be used via the <code class="language-plaintext highlighter-rouge">QUERY</code> method, and for <code class="language-plaintext highlighter-rouge">POST</code> it supports
both standard file uploads and some custom JSON-based format.</p>
<p>Typically you wouldn’t find all of these at the same endpoint, but I wanted
to show a few examples together.</p>
<h2 id="wheres-put">Where’s PUT?</h2>
<p>Oddly, there’s no specific header for <code class="language-plaintext highlighter-rouge">PUT</code> requests. Arguably you could say
that <code class="language-plaintext highlighter-rouge">GET</code> and <code class="language-plaintext highlighter-rouge">PUT</code> are symmetrical, so perhaps the <code class="language-plaintext highlighter-rouge">Accept</code> header kind of
extends to both. But the spec is not clear on this.</p>
<p>I think the actual reality is that <code class="language-plaintext highlighter-rouge">Accept-Patch</code> was the first header in
this category that really clearly defined this as a means of feature discovery
on <code class="language-plaintext highlighter-rouge">OPTIONS</code>. <code class="language-plaintext highlighter-rouge">Accept-Post</code> and <code class="language-plaintext highlighter-rouge">Accept-Query</code> followed suit. I think
<code class="language-plaintext highlighter-rouge">Accept-Patch</code> in <code class="language-plaintext highlighter-rouge">OPTIONS</code> was modelled after in-the-wild usage of <code class="language-plaintext highlighter-rouge">Accept</code>
in <code class="language-plaintext highlighter-rouge">OPTIONS</code>, even though the HTTP specific doesn’t super clearly define this.</p>
<p>If I’m wrong with my interpretation here, I would love to know!</p>
<p><small><em>
Aside: If you’re wondering about <code class="language-plaintext highlighter-rouge">DELETE</code>, <code class="language-plaintext highlighter-rouge">DELETE</code> should never have a body,
so all a user would need to know is <em>can</em> they delete, which you can see
in the <code class="language-plaintext highlighter-rouge">Allow</code> header.
If this is new to you to, <a href="https://www.rfc-editor.org/rfc/rfc8631.html#section-4.2" title="service-desc link relationship">read my other article</a> about <code class="language-plaintext highlighter-rouge">GET</code> request
bodies. Most of the information there is applicable to <code class="language-plaintext highlighter-rouge">DELETE</code> as well.
</em></small></p>
<h2 id="linking-to-documentation">Linking to documentation</h2>
<p>The <code class="language-plaintext highlighter-rouge">OPTIONS</code> response is also a great place to tell users where to find
additional documentation. In the below example, I included both a
machine-readable link to a documentation site, a link to an OpenAPI definition,
and a message intended for humans in the response body:</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">200</span> <span class="ne">OK</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Mon, 23 Sep 2024 04:45:38 GMT</span>
<span class="na">Allow</span><span class="p">:</span> <span class="s">GET, QUERY, OPTIONS</span>
<span class="na">Link</span><span class="p">:</span> <span class="s"><https://docs.example.org/api/some-endpoint>; rel="service-doc"</span>
<span class="na">Link</span><span class="p">:</span> <span class="s"><https://api.example.org/openapi.yml>; rel="service-desc" type="application/openapi+yaml"</span>
<span class="na">Content-Type</span><span class="p">:</span> <span class="s">text/plain</span>
Hey there!
Thanks for checking out this API. You can find the docs for this
specific endpoint at: https://docs.example.org/api/some-endpoint
Cheers,
The dev team
</code></pre></div></div>
<p>I recommend keeping the response body as mostly informal and minimal
any real information should probably just live on its own URL and be linked to.</p>
<p>I used the <a href="https://www.rfc-editor.org/rfc/rfc8631.html#section-4.1" title="service-doc link relationship"><code class="language-plaintext highlighter-rouge">service-doc</code></a> and <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" title="Mime types"><code class="language-plaintext highlighter-rouge">service-desc</code></a> link relationships here,
but you can of course use any of the <a href="https://www.iana.org/assignments/link-relations/link-relations.xhtml" title="IANA link relations registry">IANA link relationship types</a> here
or a custom one. Also see the <a href="https://datatracker.ietf.org/doc/html/rfc8288" title="Web Linking">Web linking</a> spec for more info.</p>
<h2 id="obscure-uses">Obscure uses</h2>
<h3 id="webdav-usage">WebDAV usage</h3>
<p>WebDAV, CalDAV and CardDAV also use OPTIONS for feature discovery. For example:</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span> <span class="m">204</span> <span class="ne">No Content</span>
<span class="na">Date</span><span class="p">:</span> <span class="s">Mon, 23 Sep 2024 05:01:50 GMT</span>
<span class="na">Allow</span><span class="p">:</span> <span class="s">GET, PROPFIND, ACL, PROPPATCH, MKCOL, LOCK, UNLOCK</span>
<span class="na">DAV</span><span class="p">:</span> <span class="s">1, 2, 3, access-control, addressbook, calendar-access</span>
</code></pre></div></div>
<h3 id="the-server-wide-asterisk-request">The server-wide asterisk request</h3>
<p>Normally HTTP requests are made to a path on the server, and the first line
looks a bit like the following in HTTP/1.1:</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">GET</span> <span class="nn">/path</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</code></pre></div></div>
<p>But, there are a few other “request line” formats that are rarely used. One of
them lets you discover features available on an entire server, using the
asterisk:</p>
<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">OPTIONS</span> <span class="nn">*</span> <span class="k">HTTP</span><span class="o">/</span><span class="m">1.1</span>
</code></pre></div></div>
<p>The asterisk here is not a path. Normally asterisks aren’t even allowed in
URIs. Many HTTP clients (including <code class="language-plaintext highlighter-rouge">fetch()</code>) don’t even support this request.</p>
<p>Classic webservers like Apache and Nginx should support this. To try it out,
use CURL</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-vX</span> OPTIONS <span class="nt">--request-target</span> <span class="s1">'*'</span> http://example.org
</code></pre></div></div>
<h2 id="final-notes">Final notes</h2>
<p>If you have a reason to allow clients to discover features on an endpoint,
consider using <code class="language-plaintext highlighter-rouge">OPTIONS</code> instead of a proprietary approach! As you can
see in many of these examples, it’s especially useful if you use <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types" title="Mime types">mimetypes</a>
well.</p>
<p>If you have questions, other novel uses of <code class="language-plaintext highlighter-rouge">OPTIONS</code> or other ideas around
feature discovery, you can respond via:</p>
<ul>
<li><a href="https://indieweb.social/@evert/113317676213353403">This post on Mastodon</a></li>
<li><a href="https://bsky.app/profile/evertpot.com/post/3l6n7363zii23">This post on Bluesky</a></li>
<li>Via the <a href="https://www.w3.org/TR/webmention/">Webmention</a> protocol!</li>
</ul>
http://evertpot.com/structured-fields-javascript-v2/New Structured Fields RFC out, and so is my Javascript package2024-10-03T15:56:31+00:00Evert Pot[email protected]<p>A new RFC was released for Structured Fields: <a href="https://www.rfc-editor.org/rfc/rfc9651.html" title="Structured Field Values for HTTP">RFC9651</a>.</p>
<h2 id="what-is-it">What is it?</h2>
<p>HTTP headers have been a bit of a free-for all in terms of how complex values
are encoded, with many headers requiring their own mini-parser.</p>
<p>A while back an effort was started to fix this for headers going forward,
named ‘Structured Fields’. They’re called Fields and not ‘Headers’ because
HTTP has both Headers and Trailers!</p>
<p>Structured fields let you encode things like lists, dictionaries, strings,
numbers, booleans and binary data. The <a href="https://www.rfc-editor.org/rfc/rfc8941.html" title="Structured Field Values for HTTP old spec">original RFC</a> from 2021 is
pretty successful and although many existing headers can’t be retrofitted
to this format, a lot of new standards are taking advantage.</p>
<p>Some examples:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Parsed an ASCII string
Header: "foo"
# A simple string, called a 'Token' in the spec
Header: foo
# Parsed as number
Header: 5
Header: -10
Header: 5.01415
# Parsed into boolean
Header: ?1
Header: ?0
# Binaries are base64 encoded
Header: :RE0gbWUgZm9yIGEgZnJlZSBjb29raWU=:
# Items can have parameters
Header: "Hello world"; a="5"
# A simple list
Header: 5, "foo", bar, ?1
# Each element can have parameters
Header: sometoken; param1; param2=hi, 42
# A list can also contain lists itself. These are called 'inner lists' and
# use parenthesis
Header: sometoken, (innerlistitem1 innerlistitem2), (anotherlist)
# A simple dictionary
Header: fn="evert", ln="pot", coffee=?1
# Each item may have parameters too
Header: foo=123; q=1, bar=123, q=0.5
# A dictionary value may be an inner list again
Header: foo=(1 2 3)
</code></pre></div></div>
<p>The new RFC published last week adds 2 new data types: Dates and
‘Display strings’, which is a Unicode serialization that fits in the HTTP
header (and trailer) format.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Parsed into a Date object
Header: @1686634251
# A Unicode string, called a 'Display String' in the spec. They use
# percent encoding, but encode a different set of characters than
# URLs.
Header %"Frysl%C3%A2n"
</code></pre></div></div>
<h2 id="why-should-you-care">Why should you care?</h2>
<p>If you encounter these headers in the wild, it’s a really good idea to use
a standard parser. One of the reasons is that with using structured-fields,
there’s a built-in extension mechanism. You’ll want to make sure that when
a new parameter appears your application doesn’t suddenly break.</p>
<p>You may also want to define and use your own HTTP headers. The structured
fields format is a very good ‘default choice’ that removes decisions such
as ‘How should I encode a key value object’ or ‘how do I encode a UTF-8
string’.</p>
<p>With parsers popping up for every language, you don’t have to worry about
writing your own one-off formats.</p>
<h2 id="javascript-package">Javascript package</h2>
<p>I’m the maintainer of a Javascript library for Structured Fields, called
<a href="https://github.com/badgateway/structured-headers" title="Structured Fields parser/serializer for Javascript and Typescript">“structured headers”</a>, which I’ve also updated for this new RFC. I wish
I picked the name “structured-fields”, but I picked the name before the
original standard changed it’s name.</p>
<p>I’ve just released v2 of this library supporting these new types, and also
added ES Modules support.</p>
<h2 id="comments">Comments?</h2>
<p>Reply to one of these:</p>
<ul>
<li><a href="https://indieweb.social/@evert/113247162498865971" title="Mastodon post">Mastodon post</a></li>
<li><a href="https://bsky.app/profile/evertp.bsky.social/post/3l5nvxuyfv32m" title="Bluesky post">Bluesky post</a></li>
</ul>
http://evertpot.com/hello-world-kian/Hello World, meet Kian2024-10-02T15:08:31+00:00Evert Pot[email protected]<p>One week ago on September 24th my son Kian was born, after a 5 year fertility
journey with my wife Roxy. Roxy and Kian are well and I’m really excited for
everything that comes next!</p>
<p><img src="/assets/posts/kian.jpg" class="fill-width" title="Evert holding his son Kian at the day of his birth" /></p>
http://evertpot.com/webcomponent-download-counter/Creating a fake download counter with Web Components2024-07-02T16:07:33+00:00Evert Pot[email protected]<p>Over the years I’ve written several open source libraries. They’re mostly
unglamorous and utilitarian, but a bunch of them obtained got a decent
download count, so I thought it would be fun to try and get a grand total
and show a ‘live’ download counter on my blog.</p>
<p>This is how that looks like:</p>
<blockquote>
<p>My open source packages were downloaded roughly
<strong><download-counter inc-per-day="157444" date="2024-06-29T15:34:00Z">138945563</download-counter></strong> times.</p>
</blockquote>
<p>Like most live counters, this number isn’t tracked in real-time. Instead it
just uses a start number and updates the number based on an average number
of downloads.</p>
<p>One day it might be nice to make it live, but this is a static blog and this
would need proper hosting and a database.</p>
<h3 id="why-is-this-number-so-high">Why is this number so high?</h3>
<p>This data comes from NPM and Packagist, and they both
count any download. So this number doesn’t represent necessarily 138 million
users, but simply this many downloads by any means including bots, CI
environments and so on. I think for both package managers the goal was
probably not to have a realistic representation of users, but rather a number
that makes developers feel good. And I like that. It’s nice to see a number
go up and it’s still a nice proxy for relative popularity.</p>
<h2 id="the-web-component">The Web Component</h2>
<p>This seemed like a good use-case for a web component. Always wanted to build
one! I was surprised how easy it was.</p>
<p>This is how this looks in the HTML:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>
My open source packages were downloaded roughly
<span class="nt"><strong></span>
<span class="nt"><download-counter</span> <span class="na">inc-per-day=</span><span class="s">"157444"</span> <span class="na">date=</span><span class="s">"2024-06-29T15:34:00Z"</span> <span class="nt">></span>
138945563
<span class="nt"></download-counter></span>
<span class="nt"></strong></span> times.
<span class="nt"></p></span>
</code></pre></div></div>
<p>What’s nice is that if Javascript is not enabled, or Web Components are
not supported, this will just fall back on showing the static number.</p>
<p>I included 3 parameters:</p>
<ul>
<li>The last recorded download count (in the element value)</li>
<li>When that number was recorded (date)</li>
<li>Average number of downloads per day.</li>
</ul>
<p>I wanted to include the date because I only intend to update these numbers
rarely, so we need to know how many downloads have elapsed since the last
time.</p>
<p>Writing the web component was surprisingly straightforward too. Here is it
in its fully glory:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">DownloadCounter</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
<span class="nx">connectedCallback</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">count</span> <span class="o">=</span> <span class="o">+</span><span class="k">this</span><span class="p">.</span><span class="nx">textContent</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">date</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">date</span><span class="dl">'</span><span class="p">));</span>
<span class="k">this</span><span class="p">.</span><span class="nx">inc</span> <span class="o">=</span> <span class="p">(</span><span class="o">+</span><span class="k">this</span><span class="p">.</span><span class="nx">getAttribute</span><span class="p">(</span><span class="dl">'</span><span class="s1">inc-per-day</span><span class="dl">'</span><span class="p">))</span> <span class="o">/</span> <span class="p">(</span><span class="mi">3600</span> <span class="o">*</span> <span class="mi">24</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
<span class="k">this</span><span class="p">.</span><span class="nx">calculateCurrentDownloads</span><span class="p">();</span>
<span class="p">}</span>
<span class="nx">calculateCurrentDownloads</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">currentDownloads</span> <span class="o">=</span>
<span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span>
<span class="k">this</span><span class="p">.</span><span class="nx">count</span> <span class="o">+</span>
<span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span><span class="o">-</span><span class="k">this</span><span class="p">.</span><span class="nx">date</span><span class="p">)</span> <span class="o">*</span> <span class="k">this</span><span class="p">.</span><span class="nx">inc</span>
<span class="p">);</span>
<span class="c1">// Intl.NumberFormat adds thousands separators</span>
<span class="k">this</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">Intl</span><span class="p">.</span><span class="nx">NumberFormat</span><span class="p">().</span><span class="nx">format</span><span class="p">(</span><span class="nx">currentDownloads</span><span class="p">);</span>
<span class="nx">setTimeout</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">calculateCurrentDownloads</span><span class="p">(),</span>
<span class="c1">// Add some randomnes</span>
<span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span> <span class="o">*</span> <span class="mi">150</span><span class="p">)</span><span class="o">+</span><span class="mi">50</span><span class="p">,</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">customElements</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span>
<span class="dl">'</span><span class="s1">download-counter</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">DownloadCounter</span><span class="p">,</span>
<span class="p">);</span>
</code></pre></div></div>
<p>Now just include the file in the HTML and you’re good to go:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o"><</span><span class="nx">script</span> <span class="nx">type</span><span class="o">=</span><span class="dl">"</span><span class="s2">module</span><span class="dl">"</span> <span class="nx">src</span><span class="o">=</span><span class="dl">"</span><span class="s2">/assets/js/downloadcounter.mjs</span><span class="dl">"</span><span class="o">><</span><span class="sr">/script</span><span class="err">>
</span></code></pre></div></div>
<h2 id="obtaining-the-data">Obtaining the data</h2>
<p>I’ve published 20 PHP libraries on <a href="https://packagist.org/">packagist</a> and 30
Javascript libraries on <a href="https://www.npmjs.com/">NPM</a>. This is too much to
count, so instead I wrote some scripts that pull in the data for their
respective APIs.</p>
<p>You cannot easily ask these APIs what the download count or download rate at a
given date was, and for both the numbers might be a bit delayed. So I’ve opted
to simply:</p>
<ul>
<li>Get a grand total of all downloads up until this date.</li>
<li>Wait at least 24 hours or more.</li>
<li>Get another grand total and use the difference to caclulate average download rate.</li>
</ul>
<p>Below is my approach for getting the numbers from NPM and Packagist. It’s a bit
messy and imperative.</p>
<h3 id="npm">NPM</h3>
<p>The NPM api gave me weird, incomplete results for getting the whole list of packages
I’ve authored, so I worked around this by hardcoding some package names, and then
augmenting this with the result of 3 searches.</p>
<p>This gives us the full list of packages I’m interested in:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Searches</span>
<span class="kd">const</span> <span class="nx">npmSearches</span> <span class="o">=</span> <span class="p">[</span>
<span class="c1">// by author</span>
<span class="dl">'</span><span class="s1">author:evrt</span><span class="dl">'</span><span class="p">,</span>
<span class="c1">// by org name</span>
<span class="dl">'</span><span class="s1">scope:badgateway</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">scope:curveball</span><span class="dl">'</span><span class="p">,</span>
<span class="p">];</span>
<span class="c1">// Hardcoded extra packages that for some reason didn't get returned with</span>
<span class="c1">// the searches</span>
<span class="kd">const</span> <span class="nx">npmPackages</span> <span class="o">=</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">davclient.js</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">structured-headers</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">bigint-money</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">fetch-mw-oauth2</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">hal-types</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">react-ketting</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">ketting</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">html-form-enhancer</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">changelog-tool</span><span class="dl">'</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">fetchNpmPackageList</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">search</span> <span class="k">of</span> <span class="nx">npmSearches</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://registry.npmjs.org/-/v1/search?text=</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">search</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="k">for</span><span class="p">(</span><span class="kd">const</span> <span class="nx">object</span> <span class="k">of</span> <span class="nx">body</span><span class="p">.</span><span class="nx">objects</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">npmPackages</span><span class="p">[</span><span class="nx">object</span><span class="p">.</span><span class="kr">package</span><span class="p">.</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>NPM doesn’t return absolute total download counts but instead we can get a
count for a specific time range with a maximum range of 18 months. I’ve
opted to instead to get download counts per year, counting backwards for
each package until get a result of 0.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">fetchNpmDownloadCounts</span><span class="p">(</span><span class="nx">packageName</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">year</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">().</span><span class="nx">getFullYear</span><span class="p">();</span>
<span class="kd">let</span> <span class="nx">count</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">yearCount</span><span class="p">;</span>
<span class="k">do</span> <span class="p">{</span>
<span class="nx">yearCount</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetchNpmDownloadCountsByYear</span><span class="p">(</span><span class="nx">packageName</span><span class="p">,</span> <span class="nx">year</span><span class="p">);</span>
<span class="nx">count</span> <span class="o">+=</span> <span class="nx">yearCount</span><span class="p">;</span>
<span class="nx">year</span><span class="o">--</span><span class="p">;</span>
<span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="nx">yearCount</span> <span class="o">></span> <span class="mi">0</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">%s: %i</span><span class="dl">'</span><span class="p">,</span> <span class="nx">packageName</span><span class="p">,</span> <span class="nx">count</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">count</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">fetchNpmDownloadCountsByYear</span><span class="p">(</span><span class="nx">packageName</span><span class="p">,</span> <span class="nx">year</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`https://api.npmjs.org/downloads/point/</span><span class="p">${</span><span class="nx">year</span><span class="p">}</span><span class="s2">-01-01:</span><span class="p">${</span><span class="nx">year</span><span class="p">}</span><span class="s2">-12-31/</span><span class="p">${</span><span class="nx">packageName</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="k">return</span> <span class="nx">body</span><span class="p">.</span><span class="nx">downloads</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="packagist">Packagist</h3>
<p>Getting totals from packagist was considerably easier:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">packagistOrgs</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">sabre</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">evert</span><span class="dl">'</span><span class="p">,</span>
<span class="p">];</span>
<span class="kd">const</span> <span class="nx">packagistPackages</span> <span class="o">=</span> <span class="p">{};</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">fetchPackagistPackages</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">vendor</span> <span class="k">of</span> <span class="nx">packagistOrgs</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`https://packagist.org/packages/list.json?vendor=</span><span class="p">${</span><span class="nx">vendor</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="k">for</span><span class="p">(</span><span class="kd">const</span> <span class="nx">pkg</span> <span class="k">of</span> <span class="nx">body</span><span class="p">.</span><span class="nx">packageNames</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">packagistPackages</span><span class="p">[</span><span class="nx">pkg</span><span class="p">]</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">fetchPackagistDownloadCounts</span><span class="p">(</span><span class="nx">packageName</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="s2">`https://packagist.org/packages/</span><span class="p">${</span><span class="nx">packageName</span><span class="p">}</span><span class="s2">/stats.json`</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">json</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">count</span> <span class="o">=</span> <span class="nx">json</span><span class="p">.</span><span class="nx">downloads</span><span class="p">.</span><span class="nx">total</span><span class="p">;</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">%s: %i</span><span class="dl">'</span><span class="p">,</span> <span class="nx">packageName</span><span class="p">,</span> <span class="nx">count</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">count</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="putting-it-together">Putting it together</h3>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">fetchNpmPackageList</span><span class="p">();</span>
<span class="k">await</span> <span class="nx">fetchPackagistPackages</span><span class="p">();</span>
<span class="k">for</span><span class="p">(</span><span class="kd">const</span> <span class="nx">pkg</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">npmPackages</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">npmPackages</span><span class="p">[</span><span class="nx">pkg</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetchNpmDownloadCounts</span><span class="p">(</span><span class="nx">pkg</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">pkg</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">packagistPackages</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">packagistPackages</span><span class="p">[</span><span class="nx">pkg</span><span class="p">]</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetchPackagistDownloadCounts</span><span class="p">(</span><span class="nx">pkg</span><span class="p">);</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">packagesCombined</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">for</span><span class="p">(</span><span class="kd">const</span> <span class="p">[</span><span class="nx">packageName</span><span class="p">,</span> <span class="nx">downloads</span><span class="p">]</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">entries</span><span class="p">(</span><span class="nx">npmPackages</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">packagesCombined</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="na">ecosystem</span><span class="p">:</span> <span class="dl">'</span><span class="s1">npm</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">packageName</span><span class="p">,</span>
<span class="nx">downloads</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="p">[</span><span class="nx">packageName</span><span class="p">,</span> <span class="nx">downloads</span><span class="p">]</span> <span class="k">of</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">entries</span><span class="p">(</span><span class="nx">packagistPackages</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">packagesCombined</span><span class="p">.</span><span class="nx">push</span><span class="p">({</span>
<span class="na">ecosystem</span><span class="p">:</span> <span class="dl">'</span><span class="s1">packagist</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">packageName</span><span class="p">,</span>
<span class="nx">downloads</span>
<span class="p">});</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">packagesCombined</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Total: %i</span><span class="dl">'</span><span class="p">,</span> <span class="nx">packagesCombined</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">acc</span><span class="p">,</span> <span class="nx">cur</span><span class="p">)</span> <span class="o">=></span> <span class="nx">acc</span><span class="o">+</span><span class="nx">cur</span><span class="p">.</span><span class="nx">downloads</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>
<span class="p">}</span>
<span class="k">await</span> <span class="nx">main</span><span class="p">();</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>Whenever I need small bits of Javascript to enhance a web application (and
not a full-blown framework) I tend to write code that looks for an element
with a particular class, and then start my logic and hook up events.</p>
<p>Web Components seems like a really great replacement for that.</p>
<p>Got comments? Liked this aticle? You can reply to <a href="https://indieweb.social/@evert/112717778570631838">this Mastodon post</a>
to make the comments show up here.</p>
http://evertpot.com/bigint-money-2/Moving on from Mocha, Chai and nyc.2024-04-21T03:00:00+00:00Evert Pot[email protected]<p>I’m a maintainer of several small open-source libraries. It’s a fun activity.
If the scope of the library is small enough, the maintenance burden is
typically fairly low. They’re usually mostly ‘done’, and I occasionally just need to
answer a few questions per year, and do the occasional release to bring it
back up to the current ‘meta’ of the ecosystem.</p>
<p>Also even though it’s ‘done’, in use by a bunch of people and well tested,
it’s also good to do a release from time to time to not give the impression
of abandonment.</p>
<p>This weekend I released a 2.0 version of my <a href="https://github.com/evert/bigint-money">bigint-money</a> library, which
is a fast library for currency math.</p>
<p>I originally wrote this in 2018, so the big BC break was switching everything
over to <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules" title="JavaScript modules">ESM</a>. For a while I <a href="https://evertpot.com/universal-commonjs-esm-typescript-packages/">tried to support both CommonJS and ESM</a>
builds for my packages, but only a year after all that effort it frankly no
longer feels needed. I was worried the ecosystem was going to
split, but people stuck on (unsupported) versions of Node that don’t
support ESM aren’t going to proactively keep their other dependencies updated,
so CommonJS is for (and many others) in the past now. (yay!)</p>
<p>Probably <em>the single best way</em> to keep maintenance burden for packages low is
to have few dependencies. Many of my packages have 0 dependencies.</p>
<p>Reducing <code class="language-plaintext highlighter-rouge">devDependencies</code> also helps. If you didn’t know, <code class="language-plaintext highlighter-rouge">node</code> now has a
built-in testrunner. I’ve been using <a href="https://mochajs.org/">Mocha</a> + <a href="https://www.chaijs.com/">Chai</a> for many many
years. They were awesome and want to thank the maintainers, but <code class="language-plaintext highlighter-rouge">node --test</code>
is pretty good now and has pretty output.</p>
<p>It also:</p>
<ul>
<li>Is much faster (about twice as fast with Typescript and code coverage
reporting, but I suspect the difference will grow with larger code bases).</li>
<li>Easier to configure (especially when you’re also using Typescript. Just use <code class="language-plaintext highlighter-rouge">tsx --test</code>).</li>
<li>It can output test coverage with (<code class="language-plaintext highlighter-rouge">--experimental-test-coverage</code>).</li>
</ul>
<video src="/assets/video/node-test.webm" class="fill-width" controls="" loop=""></video>
<p>Furthermore, while <a href="https://nodejs.org/api/assert.html">node:assert</a> doesn’t have all features of Chai, it has
the important ones (deep compare) and adds better Promise support.</p>
<p>All in all this reduced my <code class="language-plaintext highlighter-rouge">node_modules</code> directory from a surprising 159M
to 97M, most of which is now Typescript and ESLint, and my total dependency
count from 335 to 141 (almost all of which is ESLint).</p>
<p>Make sure that Node’s test library, coverage and assertion library is right
for you. It may not have all the features you expect, but I keep my testing
setup relatively simple, so the switch was easy.</p>
http://evertpot.com/oauth2-client-updates/OAuth2 client updates2024-02-05T15:00:00+00:00Evert Pot[email protected]<p>I just released v2.3.0 of <a href="https://www.npmjs.com/package/@badgateway/oauth2-client"><code class="language-plaintext highlighter-rouge">@badgateway/oauth2-client</code></a>, which I wrote
because there weren’t any lean, 0-dependency oauth2 clients with modern
features such as <a href="https://datatracker.ietf.org/doc/html/rfc7636">PKCE</a>.</p>
<p>This new version includes support for:</p>
<ul>
<li>Resource Indicators for OAuth 2.0 (<a href="https://datatracker.ietf.org/doc/html/rfc8707" title="https://datatracker.ietf.org/doc/html/rfc8707">RFC8707</a>).</li>
<li>OAuth2 Token Revocation (<a href="https://datatracker.ietf.org/doc/html/rfc7009" title="OAuth 2.0 Token Revocation">RFC7009</a>).</li>
</ul>
<p>Hope you like it!</p>
http://evertpot.com/jsx-template/Using JSX on the server as a template engine2023-11-14T13:14:02+00:00Evert Pot[email protected]<p>The React/Next.js ecosystem is spinning out of control in terms of magic and complexity.
The stack has failed to stay focused and simple, and it’s my belief
that software stacks that are too complex and magical must eventually fail,
because as sensibilities around software design change they will be unable to
adapt to those changes without cannibalizing their existing userbase.</p>
<p>So while React/Next.js may be relegated to the enterprise and legacy systems in
a few years, they completely transformed front-end development and created ripple
effects in many other technologies. One of many great ideas stemming from this
stack is <a href="https://en.wikipedia.org/wiki/JSX_(JavaScript)">JSX</a>. I think JSX has a chance to stay relevant and useful beyond
React/Next.</p>
<p>One of it’s use-cases is for server-side templating. I’ve been using JSX as a
template engine to replace template engines like <a href="https://ejs.co/">EJS</a> and
<a href="https://handlebarsjs.com/">Handlebars</a>, and more than once people were surprised this was possible
without bulky frameworks such as Next.js.</p>
<p>So in this article I’m digging into what JSX is, where it comes from and how one
might go about using it as a simple server-side HTML template engine.</p>
<h2 id="what-is-jsx">What is JSX?</h2>
<p>JSX is an extension to the Javascript language, and was introduced with React.
It usually has a <code class="language-plaintext highlighter-rouge">.jsx</code> extension and it needs to be compiled <em>to</em> Javascript.
Most build tools people already use, like ESbuild, Babel, Vite, etc. all
support this natively or through a plugin.
Typescript also natively supports it, so if you use Typescript you can just start
using it without adding another tool.</p>
<p>It looks like this:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Hello world!<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"><</span><span class="nt">p</span><span class="p">></span>Sup<span class="p"></</span><span class="nt">p</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">>;</span>
</code></pre></div></div>
<p>As you can see here, some HTML is directly embedded into Javascript, without
quotes. It’s all syntax. It lets you use the full power of Javascript, such
as variables and loops:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">user</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">Evert</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">todos</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">Wash clothes</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">Do dishes</span><span class="dl">'</span><span class="p">,</span>
<span class="p">];</span>
<span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Hello <span class="si">{</span><span class="nx">evert</span><span class="si">}</span><span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">ul</span><span class="p">></span>
<span class="si">{</span><span class="nx">todos</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span> <span class="nx">todo</span> <span class="o">=></span> <span class="p"><</span><span class="nt">li</span><span class="p">></span>todo<span class="p"></</span><span class="nt">li</span><span class="p">>)</span><span class="si">}</span>
<span class="p"></</span><span class="nt">ul</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">>;</span>
</code></pre></div></div>
<p>It has a convention to treat tags that start with a lowercase character such
as <code class="language-plaintext highlighter-rouge"><h1></code> as output, but if the tag starts with an uppercase character,
it’s a component, which usually is represented by a function:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">HelloWorldComponent</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Hello <span class="p"><</span><span class="nt">span</span><span class="p">></span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="p"></</span><span class="nt">span</span><span class="p">></</span><span class="nt">h1</span><span class="p">></span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="p"><</span><span class="nt">section</span><span class="p">></span>
<span class="p"><</span><span class="nc">HelloWorldComponent</span> <span class="na">name</span><span class="p">=</span><span class="s">"Evert"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">section</span><span class="p">>;</span>
</code></pre></div></div>
<p>Anyway, if you’re reading this you likely knew most of this but it’s important
to state that this are all JSX features, not React.</p>
<h2 id="inspo">#Inspo</h2>
<p>JSX probably has it’s roots in <a href="https://en.wikipedia.org/wiki/ECMAScript_for_XML">E4X</a> (and more directly <a href="https://docs.hhvm.com/hack/XHP/introduction">XHP</a>). E4X was a
way to embed XML in Javascript. E4X has been supported in Firefox for years, and
was a part of ActionScript 3 onwards, but there’s a major conceptual difference
between E4X and JSX.</p>
<p>With E4X you embed XML documents as data, similar to
defining a JSON object in a Javascript file. After defining the XML document
you can manipulate it. A fictional transpiler for E4X could (as far as I can
tell) could just take the XML structure and turn it into a string and pass it to
<a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString"><code class="language-plaintext highlighter-rouge">DOMParser.parseFromString()</code></a>. In E4X there are no variables, functions,
components, or logic. Similar to how a Regular expression is part of the JS
language.</p>
<p>JSX is quite different to this. It’s fully integrated in the language and it
effectively compiles down to nested function definitions. These functions are
don’t get turned into HTML until they are called by <a href="https://legacy.reactjs.org/docs/rendering-elements.html#rendering-an-element-into-the-dom" title="Rendering an Element into the DOM">some render function</a>.
Before this they are defined but not evaluated.</p>
<p>So while E4X and JSX share that they both make XML/HTML first-class citizens
in the language, the goals and features are wildly different. JSX really is a
<a href="https://en.wikipedia.org/wiki/Domain-specific_language">DSL</a>.</p>
<h2 id="jsx-transpiled">JSX Transpiled</h2>
<p>Lets turn the previous example into Typescript, and see what Typescript does with
it:</p>
<div class="language-jsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">HelloWorldComponent</span><span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="p">{</span> <span class="nl">name</span><span class="p">:</span> <span class="nx">string</span> <span class="p">})</span> <span class="p">{</span>
<span class="k">return</span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Hello <span class="p"><</span><span class="nt">span</span><span class="p">></span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="p"></</span><span class="nt">span</span><span class="p">></</span><span class="nt">h1</span><span class="p">></span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="p"><</span><span class="nt">section</span><span class="p">></span>
<span class="p"><</span><span class="nc">HelloWorldComponent</span> <span class="na">name</span><span class="p">=</span><span class="s">"Evert"</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">section</span><span class="p">>;</span>
</code></pre></div></div>
<p>By default, Typescript will turn it into this:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">HelloWorldComponent</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">h1</span><span class="dl">"</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span>
<span class="dl">"</span><span class="s2">Hello </span><span class="dl">"</span><span class="p">,</span>
<span class="nx">React</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">span</span><span class="dl">"</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="nx">props</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="nx">foo</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">"</span><span class="s2">section</span><span class="dl">"</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span>
<span class="nx">React</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="nx">HelloWorldComponent</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Evert</span><span class="dl">"</span> <span class="p">}));</span>
</code></pre></div></div>
<p>Now, there’s definitely a react dependency here, but in recent versions of
Typescript, we can configure it to use a different ‘factory’ for JSX. By
setting the <a href="https://www.typescriptlang.org/tsconfig#jsx">jsx</a> and <a href="https://www.typescriptlang.org/tsconfig#jsxFactory">jsxFactory</a> settings in <code class="language-plaintext highlighter-rouge">tsconfig.json</code>, we
can get Typescript to output more generic code:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">HelloWorldComponent</span><span class="p">(</span><span class="nx">props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">_jsxs</span><span class="p">(</span><span class="dl">"</span><span class="s2">h1</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">children</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">Hello </span><span class="dl">"</span><span class="p">,</span> <span class="nx">_jsx</span><span class="p">(</span><span class="dl">"</span><span class="s2">span</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">children</span><span class="p">:</span> <span class="nx">props</span><span class="p">.</span><span class="nx">name</span> <span class="p">})]</span> <span class="p">});</span>
<span class="p">}</span>
<span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">_jsx</span><span class="p">(</span><span class="dl">"</span><span class="s2">section</span><span class="dl">"</span><span class="p">,</span> <span class="p">{</span> <span class="na">children</span><span class="p">:</span> <span class="nx">_jsx</span><span class="p">(</span><span class="nx">HelloWorldComponent</span><span class="p">,</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Evert</span><span class="dl">"</span> <span class="p">})</span> <span class="p">});</span>
</code></pre></div></div>
<p>So what’s <code class="language-plaintext highlighter-rouge">_jsx</code>? This is a ‘jsx factory’ function that can be provided by
React, but also a number of other libraries such as <a href="https://preactjs.com/">Preact</a> or
<a href="https://www.solidjs.com/">Solid.js</a>. It’s also relatively easy to <a href="https://lwebapp.com/en/post/custom-jsx" title="Custom JSX Factory Function">create your own</a>.</p>
<p>What’s interesting then about JSX is that while it’s syntax, it’s not
always clear what this yields:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="o"><</span><span class="nx">h1</span><span class="o">></span><span class="nx">Sup</span><span class="o">!<</span><span class="sr">/h1</span><span class="err">>
</span></code></pre></div></div>
<p>Because what’s in <code class="language-plaintext highlighter-rouge">foo</code> depends on what JSX Factory was used during
transpiling. This is frustrating because it requires out of band information
and external configuration, but also a benefit because it opens the doors
to alternative implementations.</p>
<h2 id="using-jsx-as-a-template-engine">Using JSX as a template engine</h2>
<p>When generating HTML on the server with Node, it’s pretty common to use
template engines such as <a href="https://ejs.co/">EJS</a> or <a href="https://handlebarsjs.com/">Handlebars</a>. When building a
new (multi-page) web application, it occurred to me that neither are quit
as good as JSX.</p>
<p>Some of the advantages of JSX are:</p>
<ul>
<li>Deep IDE/Language server/intellisense integration, because it’s all
syntax.</li>
<li>Static type checking with Typescript.</li>
<li>It also enforced correctly structured HTML. No way to forget to close
an element.</li>
<li>By default dynamic data is escaped. They made it challenging to get
unescaped HTML!</li>
</ul>
<p>This had me wondering, can we use server-side microframeworks such
as <a href="https://koajs.com/">Koa</a>, <a href="https://fastify.dev/">Fastify</a> or <a href="https://expressjs.com/">Express</a> but instead of string-based
template parsers and get all the benefits of JSX?</p>
<p>Turns out the answer is, yes! It’s not only pretty simple to implement,
it works exceedingly well.</p>
<p>I’ve implemented this pattern 3 times now for different frameworks, here’s
some sample code that works:</p>
<h3 id="koa">Koa</h3>
<p>Here’s an example of a small controller:</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Router</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">koa-router</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">router</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Router</span><span class="p">();</span>
<span class="nx">router</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/foo.html</span><span class="dl">'</span><span class="p">,</span> <span class="nx">ctx</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Hello world with JSX in Koa!<span class="p"></</span><span class="nt">h1</span><span class="p">>;</span>
<span class="p">});</span>
</code></pre></div></div>
<p>As you can see here we can set JSX straight on the body property. A middleware
ensures this gets transformed into HTML.</p>
<h3 id="fastify">Fastify</h3>
<p>Similar to Koa, we can use JSX instead where otherwise HTML strings would be
used:</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">FastifyInstance</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fastify</span><span class="dl">'</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">routes</span><span class="p">(</span><span class="nx">fastify</span><span class="p">:</span> <span class="nx">FastifyInstance</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">fastify</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/hello-world</span><span class="dl">'</span><span class="p">,</span> <span class="nx">request</span> <span class="o">=></span> <span class="p"><</span><span class="nt">h1</span><span class="p">></span>Sup Fastify!<span class="p"></</span><span class="nt">h1</span><span class="p">>);</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="how-does-this-magic-work">How does this magic work?</h2>
<p>For both of these I used the React library. Despite it’s ecosystem being
rather bulky, standalone React is still pretty lean and highly optimized.</p>
<p>For both of these frameworks, I simply created a middleware that checks if
the body is a React node, and if so use React’s <a href="https://react.dev/reference/react-dom/server/renderToStaticMarkup">renderToStaticMarkup</a> to do
the transformation before the response is sent.</p>
<h3 id="koa-middleware">Koa middleware</h3>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">renderToStaticMarkup</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom/server</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">isValidElement</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Context</span><span class="p">,</span> <span class="nx">Middleware</span><span class="p">,</span> <span class="nx">Next</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">koa</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">jsx</span><span class="p">:</span> <span class="nx">Middleware</span> <span class="o">=</span> <span class="k">async</span><span class="p">(</span><span class="nx">ctx</span><span class="p">:</span> <span class="nx">Context</span><span class="p">,</span> <span class="nx">next</span><span class="p">:</span> <span class="nx">Next</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">await</span> <span class="nx">next</span><span class="p">();</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">isValidElement</span><span class="p">(</span><span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="dl">'</span><span class="s1"><!DOCTYPE html></span><span class="se">\n</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">renderToStaticMarkup</span><span class="p">(</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span>
<span class="p">);</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="kd">type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">text/html; charset=utf-8</span><span class="dl">'</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>
<p>This is how it’s used:</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">application</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Koa</span><span class="p">();</span>
<span class="nx">application</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">jsx</span><span class="p">);</span>
</code></pre></div></div>
<h3 id="fastify-hooks">Fastify hooks</h3>
<p>This one needed a few more hacks, but the result is worth it:</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">onSendHookHandler</span><span class="p">,</span> <span class="nx">preSerializationHookHandler</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fastify</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">isValidElement</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">renderToStaticMarkup</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react-dom/server</span><span class="dl">'</span><span class="p">;</span>
<span class="cm">/**
* The preserialization hook lets us transform the response body
* before it's json-encoded.
*
* We use this to turn React components into an object with a ___jsx key
* that has the serialized HTML.
*/</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">preSerialization</span><span class="p">:</span> <span class="nx">preSerializationHookHandler</span><span class="o"><</span><span class="nx">unknown</span><span class="o">></span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">_request</span><span class="p">,</span> <span class="nx">reply</span><span class="p">,</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">isValidElement</span><span class="p">(</span><span class="nx">payload</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">reply</span><span class="p">.</span><span class="nx">header</span><span class="p">(</span><span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">text/html</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="p">{</span>
<span class="na">___jsx</span><span class="p">:</span> <span class="dl">'</span><span class="s1"><!DOCTYPE html></span><span class="se">\n</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">renderToStaticMarkup</span><span class="p">(</span><span class="nx">payload</span> <span class="k">as</span> <span class="kr">any</span><span class="p">)</span>
<span class="p">};</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">payload</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">};</span>
<span class="cm">/**
* The onSendHookHandler lets us transform the response body (as a string)
* We detect the ___jsx key and unwrap the HTML.
*/</span>
<span class="k">export</span> <span class="kd">const</span> <span class="nx">onSend</span><span class="p">:</span> <span class="nx">onSendHookHandler</span><span class="o"><</span><span class="nx">unknown</span><span class="o">></span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">_request</span><span class="p">,</span> <span class="nx">_reply</span><span class="p">,</span> <span class="nx">payload</span><span class="p">:</span> <span class="nx">unknown</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">payload</span><span class="o">===</span><span class="dl">'</span><span class="s1">string</span><span class="dl">'</span> <span class="o">&&</span> <span class="nx">payload</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">{"___jsx":"</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">payload</span><span class="p">).</span><span class="nx">___jsx</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">return</span> <span class="nx">payload</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>
<p>To use the hook:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">fastify</span> <span class="o">=</span> <span class="nx">Fastify</span><span class="p">();</span>
<span class="c1">// These 2 hooks allow us to render React/jsx tags straight to HTML</span>
<span class="nx">fastify</span><span class="p">.</span><span class="nx">addHook</span><span class="p">(</span><span class="dl">'</span><span class="s1">preSerialization</span><span class="dl">'</span><span class="p">,</span> <span class="nx">jsxRender</span><span class="p">.</span><span class="nx">preSerialization</span><span class="p">);</span>
<span class="nx">fastify</span><span class="p">.</span><span class="nx">addHook</span><span class="p">(</span><span class="dl">'</span><span class="s1">onSend</span><span class="dl">'</span><span class="p">,</span> <span class="nx">jsxRender</span><span class="p">.</span><span class="nx">onSend</span><span class="p">);</span>
</code></pre></div></div>
<p>Using Preact or your own factory should also totally work here.</p>
<h2 id="limitations-to-this-approach">Limitations to this approach</h2>
<p>Users of React and other frameworks may be used to pulling in data
asynchronously and updating their document. This approach only allows
for a single pass, so this means that all dynamic data to your JSX
templates needs to be fetched in advance and passed down as props.</p>
<p>This means no hooks or state.</p>
<p>To me this is an advantage because the paradigm it replaces is regular
templates, and with those the data is typically also passed in ‘at the
top’.</p>
<p>It would certainly be possible to implement Suspend and allow for
asynchronous data fetching or wait for other signals, but I haven’t
yet needed this.</p>
<h2 id="frequently-asked-questions">Frequently asked questions</h2>
<h3 id="how-do-you-handle-routing">How do you handle routing?</h3>
<p>The short answer is: you don’t. Routing is already handled by your
framework, and each route/endpoint/controller is responsible for rendering
it’s entire page.</p>
<p>To reuse things like the global layout, you use components. A slightly
fictionalized example of a page for us looks like this:</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">ctx</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p"><</span><span class="nc">PublicLayout</span><span class="p">></span>
<span class="p"><</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">h1</span><span class="p">></span>Welcome back!<span class="p"></</span><span class="nt">h1</span><span class="p">></span>
<span class="p"></</span><span class="nt">div</span><span class="p">></span>
<span class="p"><</span><span class="nt">form</span> <span class="na">method</span><span class="p">=</span><span class="s">"post"</span> <span class="na">action</span><span class="p">=</span><span class="s">"/login"</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">span</span><span class="p">></span>Email<span class="p"></</span><span class="nt">span</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"email"</span> <span class="na">name</span><span class="p">=</span><span class="s">"email"</span> <span class="na">required</span> <span class="na">minLength</span><span class="p">=</span><span class="si">{</span><span class="mi">10</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">span</span><span class="p">></span>Password<span class="p"></</span><span class="nt">span</span><span class="p">></span>
<span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="p">=</span><span class="s">"password"</span> <span class="na">name</span><span class="p">=</span><span class="s">"password"</span> <span class="na">required</span> <span class="na">minLength</span><span class="p">=</span><span class="si">{</span><span class="mi">4</span><span class="si">}</span> <span class="p">/></span>
<span class="p"></</span><span class="nt">label</span><span class="p">></span>
<span class="p"><</span><span class="nt">button</span> <span class="na">type</span><span class="p">=</span><span class="s">"submit"</span><span class="p">></span>Log In<span class="p"></</span><span class="nt">button</span><span class="p">></span>
<span class="p"></</span><span class="nt">form</span><span class="p">></span>
<span class="p"></</span><span class="nc">PublicLayout</span><span class="p">>;</span>
</code></pre></div></div>
<p>This in effect ‘inherits’ from <code class="language-plaintext highlighter-rouge">PublicLayout</code>, which looks like this:</p>
<div class="language-tsx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">Props</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">children</span><span class="p">:</span> <span class="nx">JSX</span><span class="p">.</span><span class="nx">Element</span><span class="p">[]</span><span class="o">|</span><span class="nx">JSX</span><span class="p">.</span><span class="nx">Element</span><span class="p">;</span>
<span class="nl">className</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
<span class="p">}</span>
<span class="cm">/**
* This is the main application wrapper, shared by all HTML pages.
*/</span>
<span class="k">export</span> <span class="kd">function</span> <span class="nx">PublicLayout</span><span class="p">(</span><span class="nx">props</span><span class="p">:</span> <span class="nx">Props</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="p"><</span><span class="nt">html</span><span class="p">></span>
<span class="p"><</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">title</span><span class="p">></span>Sprout Family<span class="p"></</span><span class="nt">title</span><span class="p">></span>
<span class="p"><</span><span class="nt">link</span> <span class="na">href</span><span class="p">=</span><span class="s">"/css/main.css"</span> <span class="na">rel</span><span class="p">=</span><span class="s">"stylesheet"</span> <span class="na">type</span><span class="p">=</span><span class="s">"text/css"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">meta</span> <span class="na">name</span><span class="p">=</span><span class="s">"viewport"</span> <span class="na">content</span><span class="p">=</span><span class="s">"width=device-width, initial-scale=1"</span> <span class="p">/></span>
<span class="p"><</span><span class="nt">script</span> <span class="na">src</span><span class="p">=</span><span class="s">"/js/utils.js"</span><span class="p">></</span><span class="nt">script</span><span class="p">></span>
<span class="p"></</span><span class="nt">head</span><span class="p">></span>
<span class="p"><</span><span class="nt">body</span> <span class="na">className</span><span class="p">=</span><span class="si">{</span><span class="nx">props</span><span class="p">.</span><span class="nx">className</span><span class="si">}</span><span class="p">></span>
<span class="si">{</span> <span class="nx">props</span><span class="p">.</span><span class="nx">children</span> <span class="si">}</span>
<span class="p"></</span><span class="nt">body</span><span class="p">></span>
<span class="p"></</span><span class="nt">html</span><span class="p">>;</span>
<span class="p">}</span>
</code></pre></div></div>
<h3 id="is-there-still-a-reason-to-use-handlebars-or-ejs">Is there still a reason to use Handlebars or EJS?</h3>
<p>I think one benefit of these template languages are they they are their own
isolated format with limited capabilities.</p>
<p>This is helpful when for example you build an application and let your own
users edit templates. Perhaps you for example have a ‘welcome email’ and want
to let your users/tenants modify it.</p>
<p>Giving them the full power of a Javascript + a DSL that needs to be transpiled
might be a negative here, because limiting features makes it easier to evolve
systems and there’s also a major security component.</p>
<p>My template engine of choice for these is probably <a href="https://handlebarsjs.com/">handlebars</a> or even
the more limited variant <a href="https://mustache.github.io/">mustache</a>.</p>
<h2 id="anyway">Anyway</h2>
<p>Hope this was interesting. If there’s interest in turning my Koa and Fastify
code into real NPM packages, let me know! Happy to do it if there’s a non-0
number of users.</p>
<p>In the future I might even be interested in developing my own JSX Factory that
allows awaiting for async components.</p>
<p>If any of this sounds interesting, you have a scathing critique or found
punctuation in the wrong place, you can leave a comment by replying
to this <a href="https://indieweb.social/@evert/111409207581858234">Mastodon post</a>.</p>
http://evertpot.com/on-80-percent-jobs/Why aren't there more 80% jobs?2023-10-12T20:29:12+00:00Evert Pot[email protected]<p>There’s been a bit of a trend recently for some companies to move to 4-day
workweeks. This is making a decent amount of noise, but the actual number of
companies offering this still seems pretty few and far between. It’s not hard
to imagine why CEOs might feel this is risky, considering that many don’t even
trust people to work from home.</p>
<p>What I don’t see much are 80% jobs, which are 32 hour jobs, at 80% salary.
This should be a low-risk proposition that I think a lot of people in the tech
industry would take, if it were an option.</p>
<h2 id="my-background">My background</h2>
<p>I grew up in the Netherlands, but I moved to Canada for my first programmer
job at age 20. I stayed for a few years, moved back and started a job there,
and eventually moved <em>back</em> to Canada again and pretty much settled here.</p>
<p>I got strong roots and professional experience in both places, but there was
one thing that surprised me most in the Netherlands that I haven’t really seen
in North America.</p>
<p>Apparently Netherlands has a very high rate of part-time careers. Statistics
suggest that they are the highest in the world by a large margin. My personal
experience matches this too. I know a number of people in
the Netherlands that don’t work full-time, and I think there are more opportunities
for part-time career jobs. I don’t see them much in North America.</p>
<iframe src="https://data.oecd.org/chart/7de9" width="860" height="645" style="border: 0; width: 100%; max-width: 100%" mozallowfullscreen="true" webkitallowfullscreen="true" allowfullscreen="true"><a href="https://data.oecd.org/chart/7de9" target="_blank">OECD Chart: Part-time employment rate, Total, % of employment, Annual, 2022</a></iframe>
<p>My first-hand experience is when I was interviewing a few years into my
career at a company named Ibuildings in Utrecht, Netherlands. After the
interview I was told I pretty much had the job, and then the interviewer
asked me if I wanted to work 4 or 5 days per week.</p>
<p>I only had Canadian work experience, so I was pretty shocked being asked
this. Picking 4 days seemed like a no-brainer to me.
I would get paid for 32 hours per week, instead of 40 and got to pick a
day of the week I wanted off (I picked Friday). At this company
I believe most people took the 4-day option. So yes, I got paid 20% less,
but with the tax bracket I was in this was closer to 10%.</p>
<p>For me it was great. 5 days actually feels like a lot, and 2 days in a
weekend never feels quite enough. A sentiment so common it’s boring talk
about it. I used the extra time to work on open source, errands, and picked
a few small freelance gigs here and there.</p>
<p>I’m leaning pretty socialist, but even with a capitalist hat on, I’m
surprised this doesn’t happen more. Why not give employees the option to
do this? Not everyone will take the option, but for those that do here’s some
advantages:</p>
<h2 id="advantages-of-offering-80-jobs">Advantages of offering 80% jobs</h2>
<ul>
<li>For the salary cost of 4 employees at 40 hours, you get 5 employees at 32
hours.</li>
<li>Those 5 employees are more rested, and may be generally happier.</li>
<li>There’s a lower risk of burn-out and attrition.</li>
<li>You get the combined experience of 5 people instead of 4.</li>
<li>Also I don’t believe for the 80% workers, you only get 80% output. Most
people just aren’t productive all the time.</li>
<li>Advertising this as a benefit in your company may also be attractive to
candidates for hiring.</li>
</ul>
<p>There are also some drawbacks. Every employee will have some overhead such as
a laptop, benefits and general admin. Probably a larger expense is the cost
to recruit, although in the long run a lower attrition rate might make up for
that a bit.</p>
<p>But my intuition tells me that this overhead is probably well worth the
benefit of a smarter, larger, happier workforce.</p>
<p>Some notes:</p>
<ul>
<li>I want to stress that working 4 days should be presented as a <em>choice</em>. Some
people prefer to work more for more money, and you don’t want to scare them.</li>
<li>Another, even more lightweight option for irriationally risk-averse
traditionalists is to offer 90% jobs, resulting in an extra day every 2
weeks.</li>
</ul>
http://evertpot.com/oauth2-usability/Does OAuth2 have a usability problem? (yes!)2023-04-27T02:18:00+00:00Evert Pot[email protected]<p>I read an <a href="https://news.ycombinator.com/item?id=35713518">interesting thread</a> on Hackernews in response to a post:
“Why is OAuth still hard in 2023”. The post and comments bring up a lot
of real issues with OAuth. The article ends with a pitch to
use the author’s product <a href="https://www.nango.dev/">Nango</a> that advertises support
for supporting OAuth2 flows for 90+ APIs and justifying the existence
of the product.</p>
<p>We don’t need 90 browsers to open 90 websites, so why
is this the case with OAuth2? In a similar vain, the popular <a href="https://www.passportjs.org/">passport.js</a>
project has 538(!) modules for authenticating with various services,
most of which likely use OAuth2. All of these are NPM packages.</p>
<p>Anyway, I’ve been wanting to write this article for a while. It’s not
a direct response to the Nango article, but it’s a similar take with
a different solution.</p>
<h2 id="my-perspective">My perspective</h2>
<p>I’ve been working on an <a href="https://github.com/curveball/a12n-server">OAuth2 server</a> for a few years now, and last
year I released an open source <a href="https://github.com/badgateway/oauth2-client">OAuth2 client</a>.</p>
<p>Since I released the client, I’ve gotten several new features and requests
that were all contributed by users of the library, a few of note are:</p>
<ul>
<li>Allowing <code class="language-plaintext highlighter-rouge">client_id</code> and <code class="language-plaintext highlighter-rouge">client_secret</code> to be sent in request bodies
instead of the <code class="language-plaintext highlighter-rouge">Authorization</code> header.</li>
<li>Allow ‘extra parameters’ to be sent with some OAuth2 flows. Many servers,
including Auth0 require these.</li>
<li>Allow users to add their own HTTP headers, for the same reason.</li>
</ul>
<p>What these have in common is that there’s a lot of different OAuth2 servers
that want things in a slightly different/specific way.</p>
<p>I kind of expected this. It wasn’t going to be enough to just implement
OAuth2. This library will only work once people start trying it with different
servers and run into mild incompatibilities that this library will have to
add workarounds for.</p>
<p>Although I think OAuth2 is pretty well defined, the full breadth of specs and
implementations makes it so that it’s not enough to (as an API developer) to just
tell your users: “We use OAuth2”.</p>
<p>For the typical case, you might have to tell them something like this:</p>
<ul>
<li>We use OAuth2.</li>
<li>We use the <code class="language-plaintext highlighter-rouge">authorization_code</code> flow.</li>
<li>Your <code class="language-plaintext highlighter-rouge">client_id</code> is X.</li>
<li>Our ‘token endpoint’ is Y.</li>
<li>Our ‘authorization endpoint’ is Z.</li>
<li>We require PKCE.</li>
<li>Requests to the “token” endpoint require credentials to be sent in a body.</li>
<li>Any custom non-standard extensions.</li>
</ul>
<p>To some extent this is by design. The OAuth2 spec calls itself: “The OAuth 2.0
Authorization Framework”. It’s not saying it is <em>the</em> protocol, but rather it’s
a set of really good building blocks to implement your own authentication.</p>
<p>But for users that want to use generic OAuth2 tooling this is not ideal.
Not only because of the amount of information that needs to be shared, but also
it requires users of your API to be familiar with all these terms.</p>
<p>A side-effect of this is that API vendors that use OAuth2 will be more likely
roll their own SDKs, so they can insulate users from these implementation details.
It also creates a market for products like Nango and Passport.js.</p>
<p>Another result is that I see <em>many</em> people invent their own authentication flows
with JWT and refresh tokens from scratch, even though OAuth2 would be good fit.
Most people only need a small part of OAuth2, but to understand <em>which</em> small
part you need you’ll need to wade through and understand a dozen IETF RFC
documents, some of wich are still drafts.</p>
<p><em>Sidenote: <a href="https://openid.net/connect/">OpenID Connect</a> is another dimension on top of this. OpenID Connect builds on
OAuth2 and adds many features and another set of dense technical specs that are
(in my opinion) even harder to read.</em></p>
<p>OAuth2 as a framework is really good and very successful. But it’s not as good
at being a generic protocol that people can write generic code for.</p>
<h2 id="solving-the-setup-issue">Solving the setup issue</h2>
<p>There’s a nice OAuth2 feature called “OAuth 2.0 Authorization Server Metadata”,
defined in <a href="https://www.rfc-editor.org/rfc/rfc8414">RFC8414</a>. This is a JSON document sitting at a predictable URL:
<code class="language-plaintext highlighter-rouge">https://your-server/.well-known/oauth-authorization-server</code>, and can tell
clients:</p>
<ul>
<li>Which flows and features are supported</li>
<li>How to pass credentials</li>
<li>URLs to every endpoint.</li>
</ul>
<p>Here’s an example from my server:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"issuer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8531"</span><span class="p">,</span><span class="w">
</span><span class="nl">"authorization_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/authorize"</span><span class="p">,</span><span class="w">
</span><span class="nl">"token_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/token"</span><span class="p">,</span><span class="w">
</span><span class="nl">"token_endpoint_auth_methods_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"client_secret_basic"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"token_endpoint_auth_signing_alg_values_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"RS256"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"jwks_uri"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8531/.well-known/jwks.json"</span><span class="p">,</span><span class="w">
</span><span class="nl">"scopes_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"openid"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"response_types_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"token"</span><span class="p">,</span><span class="w">
</span><span class="s2">"code"</span><span class="p">,</span><span class="w">
</span><span class="s2">"code id_token"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"grant_types_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"client_credentials"</span><span class="p">,</span><span class="w">
</span><span class="s2">"authorization_code"</span><span class="p">,</span><span class="w">
</span><span class="s2">"refresh_token"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"id_token_signing_alg_values_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"RS256"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"service_documentation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:8531"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ui_locales_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"en"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"introspection_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/introspect"</span><span class="p">,</span><span class="w">
</span><span class="nl">"revocation_endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/revoke"</span><span class="p">,</span><span class="w">
</span><span class="nl">"revocation_endpoint_auth_methods_supported"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"client_secret_basic"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>If your server and client supports this, it can simplify the setup a great
deal. Here’s an example using my client:</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">OAuth2Client</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@badgateway/oauth2-client</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OAuth2Client</span><span class="p">({</span>
<span class="na">server</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://my-auth-server/</span><span class="dl">'</span><span class="p">,</span>
<span class="na">clientId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">my-client-id</span><span class="dl">'</span>
<span class="p">});</span>
</code></pre></div></div>
<p>The problem is majority of OAuth2 servers and clients don’t support this,
so even if your server did, your setup instructions would still need to
include all the ‘setup info’ for clients that don’t support the discovery
document.</p>
<p>And for the clients that <em>do</em> support it, you would need to call out that
your users’ client needs support for RFC8414.</p>
<p>So while I think the discovery spec is great and solves real problems;
it alone cannot solve the OAuth2 usability problem.</p>
<h2 id="my-proposal">My proposal</h2>
<p>Currently work is underway to define <a href="https://www.ietf.org/archive/id/draft-ietf-oauth-v2-1-08.html">OAuth 2.1</a>. OAuth 2.1 will remove
features that are considered insecure or bad practices (such as <code class="language-plaintext highlighter-rouge">implicit</code>
and <code class="language-plaintext highlighter-rouge">password</code> flows) and PKCE is brought in as a core feature.</p>
<p>If you’re a OAuth 2 client or server maintainer and kept up with the specs,
you likely are already compatible with OAuth 2.1.</p>
<p>I don’t think OAuth 2.1 goes far enough.</p>
<p>I think that this proposal should <em>require</em> support for the discovery document
and make it a required step to find features and endpoints. I also think
it should have an opinion on how clients should support custom extensions and
how they might work. (or forbid them).</p>
<p>This version of OAuth should also provide a way to discover the discovery
endpoint (hear me out).</p>
<p>If a client makes a HTTP request to an API, and the API replies with <code class="language-plaintext highlighter-rouge">401</code>, it
should tell the client where to find the discovery document and which
flow(s) to use.</p>
<p>Lastly, I think that it should be renamed to OAuth 3, so API vendors no longer
have to state:</p>
<ul>
<li>We use OAuth 2.</li>
<li>We use the <code class="language-plaintext highlighter-rouge">authorization_code</code> flow.</li>
<li>Your <code class="language-plaintext highlighter-rouge">client_id</code> is X.</li>
<li>Our ‘token endpoint’ is Y.</li>
<li>Our ‘authorization endpoint’ is Z.</li>
<li>We require PKCE.</li>
<li>Requests to the “token” endpoint require credentials to be sent in a body.</li>
<li>Any custom non-standard extensions.</li>
</ul>
<p>But instead:</p>
<ul>
<li>We use OAuth 3.</li>
<li>Your <code class="language-plaintext highlighter-rouge">client_id</code> is X.</li>
</ul>
<p>A nice aspect of this proposal is that OAuth 2 clients can still talk to
OAuth 3 servers, it also doesn’t obsolete the OAuth 2 framework.</p>
<p>Perhaps you could name this “OAuth 2.1: the good parts”, but I think increasing
the major version number sends a clear signal to users they should be looking
for a OAuth 3 library.</p>
<p>Then <em>maybe</em>, 10 years from now we no longer need 538 Passport.js modules for
538 APIs. Perhaps browsers could even facilitate authentication.</p>
<h2 id="final-notes">Final notes</h2>
<ul>
<li>A future OAuth version should also explicitly allow <code class="language-plaintext highlighter-rouge">http://localhost</code> as
a <code class="language-plaintext highlighter-rouge">redirect_url</code>. We need to be able to test.</li>
<li>I’m aware that there was a OAuth 3 proposal, which is now called <a href="https://openid.net/connect/">XYZ (or
maybe GNAP?)</a>. I’m not very familiar with this proposed protocol.</li>
<li><a href="https://xkcd.com/927/">XKCD 927</a> is funny, but ultimately a conversation stopper and a bit
cynical.</li>
</ul>
http://evertpot.com/switching-to-fedora/Switching to Fedora from Ubuntu2023-03-30T16:00:00+00:00Evert Pot[email protected]<p>It seems like every 7-8 years I’m switching operating systems. In 2006 I first
started using Apple, because it was just so damn cool to have a well working
Unix-like system. (I’ll never forget you Snow Leopard).</p>
<p>In 2015 I switched to Ubuntu. Apple’s Software seemed to hit rock bottom at
this point from a quality perspective, and hardware seemed to go obsolete at
a rate I hadn’t seen before. Fed up paying a premium price for a sub-par
product, it was <a href="https://evertpot.com/switching-to-linux/">time for a change</a>.</p>
<p><img style="float: left; max-width: 25%; padding: 0 10px 10px 0" alt="Ubuntu's logo deterioated" src="/assets/posts/fedora/ubuntu-fried.png" /></p>
<h2 id="ubuntus-fall">Ubuntu’s fall</h2>
<p>Ubuntu was the obvious choice. I want something that just works, and <a href="https://www.dell.com/community/Developer-Blogs/Dell-XPS-13-Plus-developer-edition-with-Ubuntu-22-04-LTS-pre/ba-p/8255332">Dell’s
XPS 13 Developer Edition</a> ships with Ubuntu which means hardware support
from Dell itself. Breath of fresh air and fast as hell. The experience was
similar to what people have been saying about the new M1 chips.
But it’s fast because of software, not hardware.</p>
<p>But something changed with Ubuntu in recent years. I think linux users have
a thicker than usual skin when it comes to software issues and are willing
to look past more things. But Ubuntu’s quality has been consistently falling.
I was being worn down, and it seems to be a common sentiment in my bubble.</p>
<p>The best example is Ubuntu’s package manager Snap. A pretty good idea, I like
the branding too but the execution hasn’t been there. Ubuntu users have been
effectively beta-testing this for years. Just to give you an idea I made a
<a href="https://evertpot.com/firefox-ubuntu-snap/">giant list of bugs</a> that I ran into when Ubuntu switched Firefox from apt
to Snap.</p>
<p>To be honest I feel a bit bad ragging on Ubuntu, because without knowing
<em>anything</em> about how the project and Canonical is run, my intuition is
that the steam is just kind of running out for them. Ubuntu feels ‘depressed’,
but maybe it’s all in my head.</p>
<p><img style="float: right; max-width: 25%; padding: 0 0 0 20px" alt="Fedora logo" src="/assets/posts/fedora/fedora.svg" /></p>
<h2 id="onto-fedora">Onto Fedora</h2>
<p>So I pledged to try something new in 2022, and it took me another 14 months
to find the energy and motivation to actually follow through.</p>
<p>I went for Fedora. Named after a fashion faux pas, it seems to be on the #2
spot for desktop linux. It’s funded by Red Hat, and seems to have a focus on
keeping it’s packages very up to date, which I like and why I originally
switched from Debian to Ubuntu!</p>
<p>I’m a week and a bit in, and here are my first impressions.</p>
<h3 id="installation">Installation</h3>
<p>Installation was super smooth. I always forget to make a separate <code class="language-plaintext highlighter-rouge">/home</code>
mount, so it took a while to move everything to an external disk and back.</p>
<p>The <em>one</em> thing I always forget to move is my MySQL databases, and today
was no exception.</p>
<h3 id="non-free-stuff">Non-free stuff</h3>
<p>Fedora does not ship with things that aren’t open source. Nothing against that
philosophy (awesome in fact), but personally I don’t mind adding some binaries
for a better experience.</p>
<p>I miss Ubuntu’s <a href="https://askubuntu.com/questions/47506/how-do-i-install-additional-drivers">Additional Drivers</a> tool, because it told me what to
install. I’m sure the drivers I need are available for Fedora, but I don’t
know what to look for which makes me slightly worried my computer is not
running optimally. Battery feels worse but that could also be my imagination.</p>
<p>Video in Firefox didn’t work at all in stock Fedora. I had to install
ffmpeg to get it to barely function, but then I discovered <a href="https://rpmfusion.org/">RPM Fusion</a>, where
I got an even better ffmpeg, plus gstreamer and Intel drivers and I can now watch
beautiful smooth 4K video, and confirmed with <code class="language-plaintext highlighter-rouge">intel_gpu_top</code> that I’m using
hardware acceleration.</p>
<p><a href="/assets/posts/fedora/gpu-top.png"><img class="fill-width" alt="intel_gpu_top output" src="/assets/posts/fedora/gpu-top.png" /></a></p>
<h3 id="gnome">Gnome</h3>
<p>Ubuntu used to have their own desktop environment called Unity. In
2018 they switched to Gnome, but they modified Gnome to keep their
Unity look.</p>
<p><a href="/assets/posts/fedora/ubuntu.png"><img class="fill-width" alt="Ubuntu 22.10 look" src="/assets/posts/fedora/ubuntu.png" /></a></p>
<p>This felt like a good move, because it let them kept their look while
taking advantage of all the Gnome plumbing.</p>
<p>One drawback is that Ubuntu was usually a bit behind with Gnome
features.</p>
<p>Fedora uses stock Gnome. As a result there’s more consistency overall,
and it’s nice to have the latest features. I miss the strong visual
identity Ubuntu has though. It sets itself apart from Mac and Windows.</p>
<p><a href="/assets/posts/fedora/fedora.png"><img class="fill-width" alt="Gnome's neutral colors" src="/assets/posts/fedora/fedora.png" /></a></p>
<p>I’m sure I can customize Fedora to be more fun, but if I’m being real
with myself I haven’t changed my desktop background in 10 years,
and so this will never happen.</p>
<h3 id="flatpak">Flatpak</h3>
<p>The only thing that Flatpak had to do to be better than Snap
was to not remind me of its existence unless I’m installing something, and
so far it’s done that. <a href="https://flathub.org/home">Flathub</a> is nice, and I love the idea of developers
not having to repackage for every distro under the sun.</p>
<h3 id="bugginess">Bugginess</h3>
<p>Fedora does generally feel more stable. Fewer background processes
pegged at 100%. 3rd party application support doesn’t seem as good on
first sight. I had to find workarounds for <a href="https://keepassxc.org/">KeeppassXC</a> and
<a href="https://flathub.org/apps/details/im.riot.Riot">Element</a> to not crash all the time.</p>
<p>This is no fault of the Fedora team, but it does tell you something
about how much Fedora is on other people’s radars. If developers test
1 linux distro, it will be Ubuntu.</p>
<h3 id="software-center-app">Software Center app</h3>
<p>I falsely assumed that the Software Center application was buggy
due to Ubuntu’s Snap changes, but it also hangs all the time in Fedora.</p>
<p>The solution is to kill the <code class="language-plaintext highlighter-rouge">gnome-software</code> process if you want
to use ‘Software’ more than once per session. Apparently this has
been an issue for <a href="https://www.reddit.com/r/Fedora/comments/tt04l1/hows_software_still_having_issues_like_these/">at least 12 months</a></p>
<h2 id="would-i-recommend-fedora">Would I recommend Fedora?</h2>
<p>Things are never perfect, but even with some of the issues I ran into
I’m very happy I switched. It’s hard to describe, but things feel more
solid.</p>
<p>To get to this state I did have to put in some work and research, but not
at a level of recompiling kernels.</p>
<p>If you are comfortable googling, using the terminal for some commands
and interpreting an occasional error message I would now recommend Fedora
over Ubuntu.</p>
<p>If you are not a technical user or programmer-adjacent, I think Ubuntu
is still going to be the choice with the least amount of friction. In
large I think this is simply because it is the biggest, has the most
eyes and the most support.</p>
<p>I’m excited though. Fresh start!</p>
http://evertpot.com/universal-commonjs-esm-typescript-packages/Supporting CommonJS and ESM with Typescript and Node2023-03-21T17:11:00+00:00Evert Pot[email protected]<p>I maintain a few dozen Javascript libraries, and recently updated many of them
to support CommonJS and Ecmascript modules at the same time.</p>
<p>The first half of this article describes why and how I did it, and then all the
hoops I had to jump through to make things work.</p>
<p>Hopefully it’s a helpful document for the next challenger.</p>
<p>A quick refresher, CommonJS code typically looks like this:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">MyApp</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./my-app</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span><span class="na">foo</span><span class="p">:</span> <span class="mi">5</span><span class="p">};</span>
</code></pre></div></div>
<p>And ESM like this:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">MyApp</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./my-app.js</span><span class="dl">'</span><span class="p">;</span>
<span class="k">export</span> <span class="k">default</span> <span class="p">{</span><span class="na">foo</span><span class="p">:</span> <span class="mi">5</span><span class="p">};</span>
</code></pre></div></div>
<p>Except if you use Typescript! Most Typescript uses ESM syntax, but actually
builds to CommonJS. So if your Typescript code looks like the second and
think ‘I’m good’, make sure you also take a look at the built Javascript
code to see what you actually use.</p>
<h2 id="why-support-esm">Why support ESM</h2>
<p>The general vibe is that ESM is going to be the future of Javascript code.
Even though I don’t think the developer experience is quite there yet, but
more people will start trying ESM.</p>
<p>If you decided to plunge in with ESM, I want my libraries to feel first-class.</p>
<p>For example, I’d want you to be able to default-import:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Controller</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@curveball/controller</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>At the same time most people are still on CommonJS, and I want to continue
to support this without breaking backwards compatibility for the forseeable
future.</p>
<h2 id="the-general-approach">The general approach</h2>
<p>My goal is for my packages to ‘default’ to ESM. From the perspective of a
library maintainer you will be dealing with ESM.</p>
<p>During the build phase steps are being made to generate a secondary CommonJS
build. As a maintainer you might run into CommonJS-specific issues, but
I suspect people will only see those if the CI system reports an issue.</p>
<p>Our published NPM packages will have a directory structure roughly like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- package.json
- tsconfig.json
- src/ # Typescript source
- cjs/ # CommonJS
- esm/ # ESM
- test/
</code></pre></div></div>
<p>We include the original typescript sources to make step-through debugging work
well, and have a seperate directory for the ESM and CommonJS builds.</p>
<p>If you just want to skip to the end and see an example of a package that has
all this work done, check out my <code class="language-plaintext highlighter-rouge">@curveball/core</code> package:</p>
<ul>
<li>Github source: <a href="https://github.com/curveball/core">https://github.com/curveball/core</a></li>
<li>Published NPM package contents: <a href="https://www.npmjs.com/package/@curveball/core?activeTab=code">https://www.npmjs.com/package/@curveball/core?activeTab=code</a></li>
</ul>
<h2 id="typescript-features-we-dont-use">Typescript features we don’t use</h2>
<h3 id="esmoduleinterop">esModuleInterop</h3>
<p>We don’t use the <code class="language-plaintext highlighter-rouge">esModuleInterop</code> setting in <code class="language-plaintext highlighter-rouge">tsconfig.json</code>. This flag
lets you default-import non-ESM packages like this:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:path</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>instead of the more awkward:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:path</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>On the surface <code class="language-plaintext highlighter-rouge">esModuleInterop</code> seems like it would be helpful, but it
has a major problem: if our libraries <em>use</em> <code class="language-plaintext highlighter-rouge">esModuleInterop</code>, anyone
who uses that library is forced to also turn it on.</p>
<p>So turning by turning <code class="language-plaintext highlighter-rouge">esModuleInterop</code> off we don’t force anyone
downstream into a specific setting. I generally recommend anyone writing
libraries to turn this off to reduce friction and ironically increase
interopability.</p>
<h3 id="path-mapping">Path mapping</h3>
<p>Typescript lets you map arbitrary paths to specific directories in your
source. This is popular because it lets you do things like:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">MyController</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@controllers/my</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>Instead of being forced to always do relative import</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">MyController</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../../../controllers/my</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>This is quite a popular feature for larger code-bases because people find
relative paths annoying.</p>
<p>I’m also not using this feature. A big issue is that only Typescript
is aware of this configuration, so any other tooling that uses your files
may need to be configured separately to also understand this, which
doesn’t always work.</p>
<p>So while I don’t have an exact list of exact reasons or configurations
where this fails, it’s a common enough issue that I’ve decided to just
avoid this feature altogether, for the sake of reducing magic. If you
do insist on using path mapping, you’re on your own.</p>
<h2 id="configuring-packagejson">Configuring package.json</h2>
<p>Modern node versions now have <a href="https://nodejs.org/api/packages.html#exports">a syntax</a> in <code class="language-plaintext highlighter-rouge">package.json</code> that lets you
specify both the default CommonJS and ESM files. These are the relevant
lines of one of our packages:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"@curveball/controller"</span><span class="p">,</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.5.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"A simple controller pattern for Curveball.js"</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"module"</span><span class="p">,</span><span class="w">
</span><span class="nl">"exports"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"require"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./cjs/index.js"</span><span class="p">,</span><span class="w">
</span><span class="nl">"import"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./esm/index.js"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cjs/index.js"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>When specifying your <code class="language-plaintext highlighter-rouge">package.json</code> this way, Node will automatically load
the correct (cjs/esm) directory depending on what the user needs. This all
happens automatically. Neat!</p>
<p><code class="language-plaintext highlighter-rouge">main</code> is no longer used, but I’m keeping it in case of older tooling.</p>
<h2 id="building-with-typescript">Building with Typescript</h2>
<p>Before we made this change, we just had a <code class="language-plaintext highlighter-rouge">src/</code> and <code class="language-plaintext highlighter-rouge">dist/</code> directory
for Typescript and Javascript files respectively. To do the build, we
could just run <code class="language-plaintext highlighter-rouge">npx tsc</code>.</p>
<p>This still works, except <code class="language-plaintext highlighter-rouge">npx tsc</code> now builds to <code class="language-plaintext highlighter-rouge">esm</code> for the regular
developer flow.</p>
<p>But when we build for creating the NPM package, we need to create both.
We use a <code class="language-plaintext highlighter-rouge">Makefile</code> for this, but these are roughly the commands to run:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npx tsc <span class="nt">--module</span> commonjs <span class="nt">--outDir</span> cjs/
<span class="nb">echo</span> <span class="s1">'{"type": "commonjs"}'</span> <span class="o">></span> cjs/package.json
npx tsc <span class="nt">--module</span> es2022 <span class="nt">--outDir</span> esm/
<span class="nb">echo</span> <span class="s1">'{"type": "module"}'</span> <span class="o">></span> esm/package.json
</code></pre></div></div>
<p>As you can also see that both our <code class="language-plaintext highlighter-rouge">cjs</code> and <code class="language-plaintext highlighter-rouge">esm</code> packages get a 1-line
<code class="language-plaintext highlighter-rouge">package.json</code> file with a <code class="language-plaintext highlighter-rouge">type</code> property.</p>
<p>This is <em>required</em> because Node needs to know wether files with a <code class="language-plaintext highlighter-rouge">.js</code>
extensions should be interpreted as CommonJS or ESM. It can’t figure it
out after opening it.</p>
<p>Node will look at the nearest <code class="language-plaintext highlighter-rouge">package.json</code> to see if the <code class="language-plaintext highlighter-rouge">type</code> property
was specified.</p>
<p>It’s also possible to use <code class="language-plaintext highlighter-rouge">.cjs</code> or <code class="language-plaintext highlighter-rouge">.mjs</code> file extensions, but this
doesn’t play well with Typescript as Typescript can’t automatically adjust
import paths for you.</p>
<h2 id="changes-we-had-to-make-in-our-code">Changes we had to make in our code</h2>
<p>I think it’s reasonable to say that that vanilla javascript and
javascript modules are slightly different programming languages</p>
<p>They are interopable, but not the same. They have slightly different
syntax and behavior. This is why you also need to tell HTML in advance
what kind of flavour of Javascript you’re going to be using:</p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><script </span><span class="na">type=</span><span class="s">"module"</span> <span class="na">src=</span><span class="s">"my-module.mjs"</span><span class="nt">></script></span>
</code></pre></div></div>
<p>So naturally I expected to have to make some changes to write code in
a way that works identical in both targets.</p>
<p>An obvious example is top-level <code class="language-plaintext highlighter-rouge">await</code> which only works in ESM. Here
are all the things I ran into:</p>
<h3 id="extensions-in-imports">Extensions in imports</h3>
<p>All our (local) typescript imports had to change from:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Foo</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>to:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Foo</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./foo.js</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>ESM <em>requires</em> extensions, whereas in CommonJS it’s not needed. Here’s a <code class="language-plaintext highlighter-rouge">sed</code>
command I used to change all these in bulk (probably not perfect, so you’ll
really want to be able to roll this back):</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find <span class="nb">.</span> <span class="nt">-name</span> <span class="s2">"*.ts"</span> | xargs <span class="nt">-n1</span> <span class="nb">sed</span> <span class="nt">-i</span> <span class="s2">"s/from '</span><span class="se">\(</span><span class="s2">.*</span><span class="se">\)</span><span class="s2">';/from '</span><span class="se">\\</span><span class="s2">1.js'/g"</span>
</code></pre></div></div>
<p>So the strange thing with all this is that your code will have statements
like:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">Foo</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./foo.js</span><span class="dl">'</span><span class="p">;</span>
</code></pre></div></div>
<p>But actually the file in that directory is called <code class="language-plaintext highlighter-rouge">./foo.ts</code> (<code class="language-plaintext highlighter-rouge">.ts</code> not <code class="language-plaintext highlighter-rouge">.js</code>)
yes, this is confusing and annoying.</p>
<p>I don’t really want a future contributor of my library to have to understand
the build chain, so it’s a leaky abstraction and a limitation of Typescript.</p>
<p>I hope this is rectified in the future. I understand the philosophy of wanting
to avoid magic as much as possible, but <em>just</em> give me a setting to globally
rewrite a <code class="language-plaintext highlighter-rouge">.ts</code> to <code class="language-plaintext highlighter-rouge">.js</code> when generating the <code class="language-plaintext highlighter-rouge">.js</code> files.</p>
<p>I secretly suspect that despite Typescript explicitly not supporting this,
this might still happen later anyway. Maybe they don’t want to commit to
building this before they have a good plan how 🤞.</p>
<h3 id="directory-imports">Directory imports</h3>
<p>In CommonJS in node you can import a directory, and if a <code class="language-plaintext highlighter-rouge">index.js</code> exists
in that directory, it will open that. ESM requires exact paths, so your</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./controllers</span><span class="dl">'</span>
</code></pre></div></div>
<p>Now has to be:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./controllers/index.js</span><span class="dl">'</span>
</code></pre></div></div>
<h3 id="__dirname-and-__filename"><code class="language-plaintext highlighter-rouge">__dirname</code> and <code class="language-plaintext highlighter-rouge">__filename</code></h3>
<p>Node defines 2 useful very variables in every CommonJS file, probably borrowed
from PHP.</p>
<p><code class="language-plaintext highlighter-rouge">__dirname</code> is a reference to the path the <em>current</em> file is in, and
<code class="language-plaintext highlighter-rouge">__filename</code> is the whole path + filename.</p>
<p>I used these to load in non-javascript assets from relative paths. For
example, I had a snippet like this to get the version of the current
package:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</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="p">;</span>
<span class="kd">const</span> <span class="nx">pkg</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</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="nx">__dirname</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">../package.json</span><span class="dl">'</span>
<span class="p">)</span>
<span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">version</span><span class="p">);</span>
</code></pre></div></div>
<p>Unfortunately, these variables no longer exist in ESM. Instead we have
<code class="language-plaintext highlighter-rouge">import.meta</code>. In Node, this is an object looking like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
url: 'file:///home/evert/src/curveball/controller/test.mjs'
}
</code></pre></div></div>
<p>So instead of a filesystem path, we get a <code class="language-plaintext highlighter-rouge">file://</code> url with the location. So to
write the equivalent in ESM we get something like this:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</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="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">url</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:url</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">pkg</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span>
<span class="k">await</span> <span class="nx">fs</span><span class="p">.</span><span class="nx">readFile</span><span class="p">(</span>
<span class="nx">url</span><span class="p">.</span><span class="nx">fileURLToPath</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">resolve</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">,</span> <span class="dl">'</span><span class="s1">./package.json</span><span class="dl">'</span><span class="p">)),</span>
<span class="dl">'</span><span class="s1">utf-8</span><span class="dl">'</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">pkg</span><span class="p">.</span><span class="nx">version</span><span class="p">);</span>
</code></pre></div></div>
<p>The challenge is writing this code in a way that will work with both. The
‘crazy’ way to solve this is to take an <code class="language-plaintext highlighter-rouge">Error</code> object, and get the path
from the string that appears in the stack trace. This means parsing the
stacktrace :(</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">url</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:url</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:path</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">function</span> <span class="nx">getDirName</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">err</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">()</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">err</span><span class="p">.</span><span class="nx">stack</span><span class="p">?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]);</span>
<span class="kd">const</span> <span class="nx">fileUrl</span> <span class="o">=</span> <span class="nx">err</span><span class="p">.</span><span class="nx">stack</span><span class="p">?.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="se">\n</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">].</span><span class="nx">match</span><span class="p">(</span><span class="sr">/at .* </span><span class="se">\((</span><span class="sr">file:.*</span><span class="se">)</span><span class="sr">:</span><span class="se">\d</span><span class="sr">+:</span><span class="se">\d</span><span class="sr">+</span><span class="se">\)</span><span class="sr">$/</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">fileUrl</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">This misrable idea failed</span><span class="dl">'</span><span class="p">);</span>
<span class="k">return</span> <span class="nx">path</span><span class="p">.</span><span class="nx">dirname</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">fileURLToPath</span><span class="p">(</span><span class="nx">fileUrl</span><span class="p">[</span><span class="mi">1</span><span class="p">]));</span>
<span class="p">}</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">getDirName</span><span class="p">());</span>
</code></pre></div></div>
<p>I don’t believe that the exact format of the stack trace is stable or even
the same across different interpreters, so this seems like a bad idea.</p>
<p>The problem is that the more obvious approach doesn’t work:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">path</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:path</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">url</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:url</span><span class="dl">'</span><span class="p">;</span>
<span class="cm">/** @ts-ignore CommonJS/ESM fun */</span>
<span class="kd">const</span> <span class="nx">dirname</span> <span class="o">=</span>
<span class="k">typeof</span> <span class="nx">__dirname</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">undefined</span><span class="dl">'</span> <span class="p">?</span>
<span class="nx">__dirname</span> <span class="p">:</span>
<span class="cm">/** @ts-ignore CommonJS/ESM fun */</span>
<span class="nx">path</span><span class="p">.</span><span class="nx">dirname</span><span class="p">(</span><span class="nx">url</span><span class="p">.</span><span class="nx">fileURLToPath</span><span class="p">(</span><span class="k">import</span><span class="p">.</span><span class="nx">meta</span><span class="p">.</span><span class="nx">url</span><span class="p">));</span>
</code></pre></div></div>
<p>The above code creates a new <code class="language-plaintext highlighter-rouge">dirname</code> variable. While
this works for ESM, it breaks for CommonJS on Node. The reason is that
<code class="language-plaintext highlighter-rouge">import</code> is not a regular object, it’s syntax and even if the branch with
<code class="language-plaintext highlighter-rouge">import.meta</code> never gets run, Node will error while parsing:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/home/evert/src/curveball/controller/cjs/dirname.js:9
path.dirname(url.fileURLToPath(import.meta.url));
^^^^
SyntaxError: Cannot use 'import.meta' outside a module
</code></pre></div></div>
<p>Sadness!</p>
<p>I don’t have a satisfying solution for this yet. I’ve worked around this in
some of my packages by avoiding this altogether, or rewriting certain files
after running them. <code class="language-plaintext highlighter-rouge">eval('import.meta.url')</code> causes the ESM build to fail
because technically things running in <code class="language-plaintext highlighter-rouge">eval</code> is no longer in a module scope.</p>
<h3 id="importing-commonjs-modules-with-default-exports">Importing CommonJS modules with ‘default exports’</h3>
<p>Some of our packages rely on 3rd party dependencies that are written in
plain Javascript and use CommonJS.</p>
<p>In CommonJS dependencies you can ‘default’ export things like:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="kd">function</span> <span class="nx">foo</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span> <span class="nx">a</span><span class="o">+</span><span class="nx">b</span><span class="p">;</span> <span class="p">};</span>
</code></pre></div></div>
<p>If we were to use this library in Typescript (CJS build) we could use it
this way:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">WebSocket</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">ws</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">ws</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WebSocket</span><span class="p">(</span><span class="dl">'</span><span class="s1">ws://localhost:8000</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>
<p>But if we are in ESM land in Typescript, this doens’t work. We actually
get an object with a property named <code class="language-plaintext highlighter-rouge">.default</code>. For these modules, this
is the approach I’ve come up with:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">WebSocketImp</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">ws</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// ESM shenanigans</span>
<span class="kd">const</span> <span class="nx">WebSocket</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">default</span><span class="dl">'</span> <span class="k">in</span> <span class="nx">WebSocketImp</span> <span class="p">?</span> <span class="p">(</span><span class="nx">WebSocketImp</span><span class="p">.</span><span class="k">default</span> <span class="k">as</span> <span class="kr">any</span><span class="p">)</span> <span class="p">:</span> <span class="nx">WebSocketImp</span><span class="p">;</span>
</code></pre></div></div>
<p>Yep! It’s a pain. I’ve had to do this for <code class="language-plaintext highlighter-rouge">websocket</code>, <code class="language-plaintext highlighter-rouge">chai-as-promised</code>,
<code class="language-plaintext highlighter-rouge">ajv</code>, <code class="language-plaintext highlighter-rouge">raw-body</code>, <code class="language-plaintext highlighter-rouge">accepts</code> and <code class="language-plaintext highlighter-rouge">http-link-header</code>. In case you haven’t
figured it out, I’m building a <a href="https://curveball.js/">framework</a>.</p>
<h2 id="testing">Testing</h2>
<p>To have confidence in both builds, I want to write tests that run against
both. My tests are written using Mocha. My tests are <em>also</em> written in
Typescript. By default, they only run against the ESM build, and I expect
myself and other developers to be in this mode during the main ‘development
loop.</p>
<p>To make Mocha understand Typescript, we need to install the <code class="language-plaintext highlighter-rouge">ts-node</code> package,
and add the following properties to the <em>root</em> <code class="language-plaintext highlighter-rouge">package.json</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"mocha"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"loader"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"ts-node/esm"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"recursive"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"extension"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"ts"</span><span class="p">,</span><span class="w">
</span><span class="s2">"js"</span><span class="p">,</span><span class="w">
</span><span class="s2">"tsx"</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>This all works perfectly well, but what about CommonJS? For this I had 2
approaches: Either reconfigure mocha to use the CommonJS Typescript
loader instead of the ESM typescript loader, but this was too much of
a pain to get right with writing configuration files on the fly.</p>
<p>Instead, I decided to just ask Typescript to build the entire project
including tests in a separate directory and then just run Mocha on
the javascript files.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> cjs-test
<span class="nb">cd test</span><span class="p">;</span> npx tsc <span class="nt">--module</span> commonjs <span class="nt">--outdir</span> ../cjs-test
<span class="nb">echo</span> <span class="s1">'{"type": "commonjs"}'</span> <span class="o">></span> cjs-test/package.json
<span class="nb">cd </span>cjs-test<span class="p">;</span> npx mocha <span class="nt">--no-package</span>
</code></pre></div></div>
<p>This required a separate <code class="language-plaintext highlighter-rouge">tsconfig.json</code> in our <code class="language-plaintext highlighter-rouge">test</code> directory.</p>
<h2 id="conclusion">Conclusion</h2>
<p>So this is a fairly large amount of annoyances to work through, some without
a solution. Would I recommend this?</p>
<p>I think if you’re in my position where:</p>
<ul>
<li>You’re doing this for a library.</li>
<li>You consider ESM the future, but want to continue to support CommonJS users.</li>
<li>The experience of users of your library is far more important than your own experience.</li>
</ul>
<p>Then, I think this is a good idea. If you don’t want this hassle and want just
one target, I think CommonJS will still give you the best experience.</p>
<p>ESM will probably catch up at some point, but right now I think we’re still in the
awkward phase.</p>
http://evertpot.com/bye-badgateway/Winding down Bad Gateway2023-02-20T19:18:15+00:00Evert Pot[email protected]<p>In 2019 I started Bad Gateway as a software development agency. Last year we
grew all the way to 7 people. It was crazy challenging, especially with Covid
in the mix; but ultimately could not get the company into a good financial
state to be able to carry on.</p>
<p>Big thanks to my co-workers and partners <a href="https://www.linkedin.com/in/juhangsin/">Ju</a>, <a href="https://becky.dev/">Becky</a>, <a href="https://www.linkedin.com/in/philippeschwyter/">Phil</a>,
<a href="https://michaelhumiston.com/">Michael</a>, <a href="http://www.lfo-industries.com/">Siep</a>, <a href="https://www.linkedin.com/in/rswitzer/">Richard</a> and <a href="https://www.linkedin.com/in/syed-farhan-kabir/">Syed</a>.
I’m incredibly grateful you came on this journey with me. We shared some
tears, exchanged some words but mostly had lots of laughs. Despite the
challenges I feel my relationship with you has only strengthened and I wish
you well in the next steps of your career.</p>
<p>Also thank you to our customers and especially <a href="https://underknown.com/">Underknown</a> who’ve stuck
with us since the start.</p>
<p>Despite this outcome, I have a hard time seeing the last few years as a
failure. It’s been hella fun, and I feel we stuck to our values even in times
of crisis. Off to the next adventure.</p>
<p><img class="fill-width" src="/assets/posts/bg-502.png" alt="Image of person standing in front of a gateway." /></p>
http://evertpot.com/node-changelog-cli-tool/Building a simple CLI tool with modern Node.js2023-02-13T19:05:14+00:00Evert Pot[email protected]<p>I’m a maintainer of several dozen open source libraries. One thing I’ve
always done is maintain a hand-written changelog.</p>
<p>Here’s an example from <a href="https://github.com/curveball/a12n-server/" title="An awesome lightweight OAuth2 server">a12n-server</a></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">0.22.0 (2022-09-27)
-------------------
</span>
Warning note for upgraders. This release has a database migration on the
<span class="sb">`oauth2_tokens`</span> table. For most users this is the largest table, some
downtime may be expected while the server runs its migrations.
<span class="p">
*</span> #425: Using a <span class="sb">`client_secret`</span> is now supported with <span class="sb">`authorization_code`</span>,
and it's read from either the request body or HTTP Basic Authorization
header.
<span class="p">*</span> The service now keeps track when issuing access tokens, whether those tokens
have used a <span class="sb">`client_secret`</span> or not, which <span class="sb">`grant_type`</span> was used to issue them
and what scopes were requested. This work is done to better support OAuth2
scopes in the future, and eventually OpenID Connect.
<span class="p">*</span> Fixed broken 'principal uri' in introspection endpoint response.
<span class="p">*</span> OAuth2 service is almost entirely rewritten.
<span class="p">*</span> The number of tokens issued is now displayed on the home page.
<span class="p">*</span> Large numbers are now abbreviated with <span class="sb">`K`</span> and <span class="sb">`M`</span>.
<span class="p">*</span> #426: Updated to Curveball 0.20.
<span class="p">*</span> #427: Typescript types for the database schema are now auto-generated with
<span class="sb">`mysql-types-generator`</span>.
</code></pre></div></div>
<p>These are all written in Markdown. You might think: isn’t Git also a log? Why
bother hand-writing these?</p>
<p>The reason is that the audience for these is a bit different. I want to bring
attention to the things that are the most important for the end-user, and
focus on the impact of the change to the user.</p>
<p>I thought it would be handy to write a CLI tool that makes it a bit easier to
maintain these. <a href="https://github.com/evert/changelog-tool" title="The changelog tool!">So, I did!</a>. If you are curious what kind of technology
choices went into this, read on.</p>
<h2 id="goals-and-features">Goals and features</h2>
<p>The tool should be able to do the following:</p>
<ul>
<li>Reformat changelogs (a bit like prettify) (<code class="language-plaintext highlighter-rouge">changelog format</code>)</li>
<li>Add an entry via the command line (<code class="language-plaintext highlighter-rouge">changelog add --minor -m "New feature"</code>).</li>
<li>Automatically set the release date (<code class="language-plaintext highlighter-rouge">changelog release</code>)</li>
<li>Pipe a log of a specific version to STDOUT, so it can be used by other tools
(like integrating with github releases).</li>
</ul>
<p>I also had a bunch of a non-functional requirements:</p>
<ul>
<li>Use the latest Node features.</li>
<li>Use up to date Javascript standards and features (ESM).</li>
<li>Avoid dependendencies unless it’s unreasonable to do so.</li>
<li>Make it low maintanance.</li>
</ul>
<p>Want to find the finished tool right now? It’s open source so just go to <a href="https://github.com/evert/changelog-tool" title="The changelog tool!">Github</a>.</p>
<h2 id="the-implementation">The implementation</h2>
<h3 id="esm--typescript">ESM & Typescript</h3>
<p>Ecmascripts modules worked really well here. It’s a small change of habits,
but the general recommendation I would have is to just save your files
as <code class="language-plaintext highlighter-rouge">.mjs</code> and start using it.</p>
<p>Here’s the first few lines of <code class="language-plaintext highlighter-rouge">parse.mjs</code>:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @ts-check</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">Changelog</span><span class="p">,</span> <span class="nx">VersionLog</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./changelog.mjs</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">readFile</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:fs/promises</span><span class="dl">'</span><span class="p">;</span>
<span class="cm">/**
* @param {string} filename
* @returns {Promise<Changelog>}
*/</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">parseFile</span><span class="p">(</span><span class="nx">filename</span><span class="p">)</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">parse</span><span class="p">(</span>
<span class="k">await</span> <span class="nx">readFile</span><span class="p">(</span><span class="nx">filename</span><span class="p">,</span> <span class="dl">'</span><span class="s1">utf-8</span><span class="dl">'</span><span class="p">)</span>
<span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The CommonJS -> ESM transition is not without pain, but for an new project
like this it’s really the ideal choice. (top level <code class="language-plaintext highlighter-rouge">await</code> 🎉)</p>
<p>I’ve also made the choice to not write my code in Typescript, but use JSDoc
annotations instead (These are the <code class="language-plaintext highlighter-rouge">@param</code> and <code class="language-plaintext highlighter-rouge">@returns</code> comments).</p>
<p>Not everyone knows that you don’t need to write .ts files to benefit from
Typescript. Typescript can also check your Javascript files quite strictly,
and there’s a lot of work still being done in adding new features to this
syntax.</p>
<p>This has the benefit of not needing a build phase. You don’t even need
Typescript during development which reduces the barrier to entry.</p>
<p>Here’s my minimal <code class="language-plaintext highlighter-rouge">tsconfig.json</code>:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"compilerOptions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"es2022"</span><span class="p">,</span><span class="w">
</span><span class="nl">"module"</span><span class="p">:</span><span class="w"> </span><span class="s2">"esnext"</span><span class="p">,</span><span class="w">
</span><span class="nl">"rootDir"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./"</span><span class="p">,</span><span class="w">
</span><span class="nl">"allowJs"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"checkJs"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"moduleResolution"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node"</span><span class="p">,</span><span class="w">
</span><span class="nl">"noEmit"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"strict"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"useUnknownInCatchVariables"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>The Typescript documentation has a <a href="https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html" title="JSDoc support in Typescript">page detailing the JSDoc annotations they
support</a>, if you’d like to learn more.</p>
<h3 id="command-line-parsing">Command line parsing</h3>
<p>CLI tools need to be able to parse command line options. Since Node 18.3
(backported to Node 16.17) Node comes with a built-in options parser.</p>
<p>Here’s a sample of the code:</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">parseArgs</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:util</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">positionals</span><span class="p">,</span> <span class="nx">values</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">parseArgs</span><span class="p">({</span>
<span class="na">options</span><span class="p">:</span> <span class="p">{</span>
<span class="na">help</span><span class="p">:</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">boolean</span><span class="dl">'</span><span class="p">,</span>
<span class="na">short</span><span class="p">:</span> <span class="dl">'</span><span class="s1">h</span><span class="dl">'</span><span class="p">,</span>
<span class="na">default</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">all</span><span class="p">:</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">boolean</span><span class="dl">'</span><span class="p">,</span>
<span class="na">default</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">message</span><span class="p">:</span> <span class="p">{</span>
<span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">,</span>
<span class="na">short</span><span class="p">:</span> <span class="dl">'</span><span class="s1">m</span><span class="dl">'</span>
<span class="p">},</span>
<span class="na">patch</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">boolean</span><span class="dl">'</span> <span class="p">},</span>
<span class="na">minor</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">boolean</span><span class="dl">'</span> <span class="p">},</span>
<span class="na">major</span><span class="p">:</span> <span class="p">{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">'</span><span class="s1">boolean</span><span class="dl">'</span> <span class="p">},</span>
<span class="p">},</span>
<span class="na">allowPositionals</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
<span class="p">});</span>
</code></pre></div></div>
<p>This confguration adds flags like <code class="language-plaintext highlighter-rouge">--major</code>, lets you specify a message
with <code class="language-plaintext highlighter-rouge">--message "hello!"</code> or use a short-hand alternative like <code class="language-plaintext highlighter-rouge">-m "Hi"</code>.</p>
<p>Does it do everything? No! There’s packages out there that are more
sophisticated, use colors, automatically create help screens but they
also ship with large dependency trees.</p>
<p>In my case, this was good enough.</p>
<p>Check out the <a href="https://nodejs.org/api/util.html#utilparseargsconfig" title="Parsing arguments with Node">Node docs</a> for more info.</p>
<h3 id="testing">Testing</h3>
<p>Most people probably use Jest or Mocha as their test framework, but since
Node 18 (also backported to 16) Node has a built-in test-runner.</p>
<p>It has an API similar to Mocha and Jest with keywords like <code class="language-plaintext highlighter-rouge">it</code>, <code class="language-plaintext highlighter-rouge">test</code>,
<code class="language-plaintext highlighter-rouge">describe</code>, <code class="language-plaintext highlighter-rouge">before</code>, etc.</p>
<p>Here’s a sample of one of my tests:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// @ts-check</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">test</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:test</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">parse</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">../parse.mjs</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">assert</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">node:assert</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">test</span><span class="p">(</span><span class="dl">'</span><span class="s1">Parsing changelog metadata</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">input</span> <span class="o">=</span> <span class="s2">`Time for a change
=========
0.2.0 (????-??-??)
------------------
* Implemented the 'list' command.
* Added testing framework.
0.1.0 (2023-02-08)
------------------
* Implemented the 'help' and 'init' commands.
*
`</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">parse</span><span class="p">(</span><span class="nx">input</span><span class="p">);</span>
<span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="dl">'</span><span class="s1">Time for a change</span><span class="dl">'</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">title</span><span class="p">);</span>
<span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">versions</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span>
<span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">versions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">date</span><span class="p">);</span>
<span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="dl">'</span><span class="s1">0.2.0</span><span class="dl">'</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">versions</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">version</span><span class="p">);</span>
<span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="dl">'</span><span class="s1">2023-02-08</span><span class="dl">'</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">versions</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nx">date</span><span class="p">);</span>
<span class="nx">assert</span><span class="p">.</span><span class="nx">equal</span><span class="p">(</span><span class="dl">'</span><span class="s1">0.1.0</span><span class="dl">'</span><span class="p">,</span> <span class="nx">result</span><span class="p">.</span><span class="nx">versions</span><span class="p">[</span><span class="mi">1</span><span class="p">].</span><span class="nx">version</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>
<p>To run the tests, just run <code class="language-plaintext highlighter-rouge">node --test</code>. No configuration needed, it will
discover tests following directory and filename conventions.</p>
<p>The Node 18 test output is a bit rough, it’s the TAP format and looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>TAP version 13
# Subtest: /home/evert/src/changelog-tool/test/parse.mjs
# Subtest: Parsing changelog metadata
ok 1 - Parsing changelog metadata
---
duration_ms: 1.713409
...
# Subtest: Parsing changelog entries
ok 2 - Parsing changelog entries
---
duration_ms: 0.2595
...
# Subtest: Preface and postface
ok 3 - Preface and postface
---
duration_ms: 0.193591
...
1..3
ok 1 - /home/evert/src/changelog-tool/test/parse.mjs
---
duration_ms: 70.901055
...
1..1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 81.481441
</code></pre></div></div>
<p>In Node 19 new test reporters will be shipped, but I haven’t tested this yet.</p>
<p>Frankly, after using this I don’t know if would use Mocha anymore. I’ve been
using probably over a decade, and it has some nice features and benefits,
for the kinds of tests I write (and I write a lot), I think there’s anything
I need beyond what Node is offering here.</p>
<p>Some links:</p>
<ul>
<li><a href="https://nodejs.org/api/test.html" title="Node Test Runner">node:test package</a>.</li>
<li><a href="https://nodejs.org/api/assert.html" title="Assertion Testing">node:assert package</a>.</li>
<li><a href="https://nodejs.org/api/test.html#mocking" title="Mocking in Node tests">Mocking in node</a>.</li>
</ul>
<h2 id="packagejson-annotated">package.json, annotated</h2>
<p>I wanted to end this article with how I’ve set up my <code class="language-plaintext highlighter-rouge">package.json</code>, so you
can see how it all ties together. (If only <code class="language-plaintext highlighter-rouge">npm</code> <a href="/json5/">supported JSON5</a> so I
could keep my comments right in the package 😭).</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">The</span><span class="w"> </span><span class="err">name</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">package</span><span class="p">,</span><span class="w"> </span><span class="err">and</span><span class="w"> </span><span class="err">how</span><span class="w"> </span><span class="err">it's</span><span class="w"> </span><span class="err">published</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">NPM</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"changelog-tool"</span><span class="p">,</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">Package</span><span class="w"> </span><span class="err">version!</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.5.0"</span><span class="p">,</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">This</span><span class="w"> </span><span class="err">will</span><span class="w"> </span><span class="err">show</span><span class="w"> </span><span class="err">up</span><span class="w"> </span><span class="err">in</span><span class="w"> </span><span class="err">NPM</span><span class="w"> </span><span class="err">searches</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"A CLI tool for manipulating changelogs"</span><span class="p">,</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">This</span><span class="w"> </span><span class="err">tells</span><span class="w"> </span><span class="err">Node</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="err">assume</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">is</span><span class="w"> </span><span class="err">an</span><span class="w"> </span><span class="err">ESM</span><span class="w"> </span><span class="err">package.</span><span class="w"> </span><span class="err">Not</span><span class="w"> </span><span class="err">strictly</span><span class="w"> </span><span class="err">needed</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">if</span><span class="w"> </span><span class="err">we</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">.mjs</span><span class="w"> </span><span class="err">extension</span><span class="w"> </span><span class="err">everywhere</span><span class="w"> </span><span class="err">though.</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"module"</span><span class="p">,</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">If</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">package</span><span class="w"> </span><span class="err">programmatically</span><span class="w"> </span><span class="err">(not</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">CLI)</span><span class="p">,</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">file</span><span class="w"> </span><span class="err">is</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">where</span><span class="w"> </span><span class="err">all</span><span class="w"> </span><span class="err">'exports'</span><span class="w"> </span><span class="err">are</span><span class="w"> </span><span class="err">for</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">package.</span><span class="w">
</span><span class="nl">"main"</span><span class="p">:</span><span class="w"> </span><span class="s2">"index.mjs"</span><span class="p">,</span><span class="w">
</span><span class="nl">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">Test</span><span class="w"> </span><span class="err">runner!</span><span class="w">
</span><span class="nl">"test"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node --test"</span><span class="p">,</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">I</span><span class="w"> </span><span class="err">like</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="err">keep</span><span class="w"> </span><span class="err">Typescript</span><span class="w"> </span><span class="err">running</span><span class="w"> </span><span class="err">in</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">terminal</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="err">insta-warn</span><span class="w"> </span><span class="err">me</span><span class="w"> </span><span class="err">of</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="err">any</span><span class="w"> </span><span class="err">issues.</span><span class="w">
</span><span class="nl">"watch"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tsc --watch"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">This</span><span class="w"> </span><span class="err">helps</span><span class="w"> </span><span class="err">people</span><span class="w"> </span><span class="err">discover</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">package</span><span class="w"> </span><span class="err">on</span><span class="w"> </span><span class="err">npmjs.org</span><span class="w">
</span><span class="nl">"keywords"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"changelog"</span><span class="p">,</span><span class="w">
</span><span class="s2">"markdown"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">It's</span><span class="w"> </span><span class="err">me!</span><span class="w">
</span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Evert Pot (https://evertpot.com/)"</span><span class="p">,</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">Do</span><span class="w"> </span><span class="err">(almost)</span><span class="w"> </span><span class="err">anything</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">want</span><span class="w">
</span><span class="nl">"license"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MIT"</span><span class="p">,</span><span class="w">
</span><span class="nl">"engine"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">Warn</span><span class="w"> </span><span class="err">people</span><span class="w"> </span><span class="err">if</span><span class="w"> </span><span class="err">they</span><span class="w"> </span><span class="err">haven't</span><span class="w"> </span><span class="err">upgraded</span><span class="w"> </span><span class="err">yet</span><span class="w">
</span><span class="nl">"node"</span><span class="p">:</span><span class="w"> </span><span class="s2">">16"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"bin"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">When</span><span class="w"> </span><span class="err">people</span><span class="w"> </span><span class="err">install</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">package</span><span class="p">,</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">makes</span><span class="w"> </span><span class="err">`npx</span><span class="w"> </span><span class="err">changelog`</span><span class="w"> </span><span class="err">work.</span><span class="w"> </span><span class="err">If</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">installed</span><span class="w"> </span><span class="err">this</span><span class="w"> </span><span class="err">package</span><span class="w"> </span><span class="err">globally</span><span class="p">,</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">now</span><span class="w"> </span><span class="err">have</span><span class="w"> </span><span class="err">a</span><span class="w"> </span><span class="err">`changelog`</span><span class="w"> </span><span class="err">command</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">at</span><span class="w"> </span><span class="err">your</span><span class="w"> </span><span class="err">disposal.</span><span class="w">
</span><span class="nl">"changelog"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./cli.mjs"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"devDependencies"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">The</span><span class="w"> </span><span class="err">only</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="err">dependencies.</span><span class="w"> </span><span class="err">You</span><span class="w"> </span><span class="err">don't</span><span class="w"> </span><span class="err">even</span><span class="w"> </span><span class="err">really</span><span class="w"> </span><span class="err">need</span><span class="w"> </span><span class="err">these</span><span class="w"> </span><span class="err">if</span><span class="w"> </span><span class="err">you</span><span class="w"> </span><span class="err">just</span><span class="w"> </span><span class="err">want</span><span class="w">
</span><span class="err">//</span><span class="w"> </span><span class="err">use</span><span class="w"> </span><span class="err">or</span><span class="w"> </span><span class="err">hack</span><span class="w"> </span><span class="err">the</span><span class="w"> </span><span class="err">package.</span><span class="w"> </span><span class="err">Github</span><span class="w"> </span><span class="err">actions</span><span class="w"> </span><span class="err">will</span><span class="w"> </span><span class="err">run</span><span class="w"> </span><span class="err">Typescript</span><span class="w"> </span><span class="err">as</span><span class="w"> </span><span class="err">well.</span><span class="w">
</span><span class="nl">"@types/node"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^18.11.19"</span><span class="p">,</span><span class="w">
</span><span class="nl">"typescript"</span><span class="p">:</span><span class="w"> </span><span class="s2">"^4.9.5"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>I love building new things and being deliberate about every choice.</p>
<p>The result is that I’m more likely to end up with something minimal,
low-maintance and I gain a deeper understanding of the tools I use.</p>
<p>In the near future I would probably make all these choices again. The Node test
runner is fast and low-cruft, ESM is great when it works and not needing a build
step feels right for a project this size.</p>
<p>I hope this inspires someone in the future to start their next project from
an empty directory instead of copying a large boilerplate project.</p>
<p><a href="https://github.com/evert/changelog-tool" title="The changelog tool!">changelog-tool project on Github</a>.</p>
http://evertpot.com/knex-sql-injection/Knex (with MySQL) had a very scary SQL injection2023-01-12T21:31:43+00:00Evert Pot[email protected]<p><a href="https://knexjs.org/">Knex</a> recently released a new version this week (2.4.0). Before this version,
Knex had a pretty scary SQL injection. Knex currently has 1.3 million weekly
downloads and is quite popular.</p>
<p>The security bug is probably one of the worst SQL injections I’ve seen in recent
memory, especially considering the scope and popularity.</p>
<p>If you want to get straight to the details:</p>
<ul>
<li><a href="https://github.com/knex/knex/issues/1227">Check out the Github issue</a>, which was opened 7 years ago(!)</li>
<li><a href="https://www.ghostccamm.com/blog/knex_sqli/">An article from Ghostccamm</a> explaining the vulnerability.</li>
<li><a href="https://nvd.nist.gov/vuln/detail/CVE-2016-20018">CVE-2016-20018</a>.</li>
</ul>
<h2 id="my-understanding-of-this-bug">My understanding of this bug</h2>
<p>If I understand the vulnerability correctly, I feel this can impact a very
large number of sites using Knex. Even more so if you use Express.</p>
<p>I’ll try to explain through a simple example. Say, you have MySQL table structured
like this:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="nv">`users`</span> <span class="p">(</span>
<span class="nv">`id`</span> <span class="nb">int</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="n">AUTO_INCREMENT</span><span class="p">,</span>
<span class="nv">`name`</span> <span class="nb">varchar</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">DEFAULT</span> <span class="k">NULL</span><span class="p">,</span>
<span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="p">(</span><span class="nv">`id`</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>
<p>And you have a query that does a <code class="language-plaintext highlighter-rouge">SELECT</code> using Knex:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">lookupId</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">knex</span><span class="p">(</span><span class="dl">'</span><span class="s1">users</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">select</span><span class="p">([</span><span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">])</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span>
<span class="na">id</span><span class="p">:</span> <span class="nx">lookupId</span>
<span class="p">});</span>
</code></pre></div></div>
<p>You’d expect the query to end up roughly like this</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="nv">`id`</span><span class="p">,</span> <span class="nv">`name`</span> <span class="k">FROM</span> <span class="nv">`users`</span> <span class="k">WHERE</span> <span class="nv">`id`</span> <span class="o">=</span> <span class="mi">2</span>
</code></pre></div></div>
<p>The issue is when the user controls the value of <code class="language-plaintext highlighter-rouge">lookupId</code>. If somehow they
can turn this into an object like this:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">lookupId</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">foo</span><span class="dl">'</span>
<span class="p">}</span>
</code></pre></div></div>
<p>You might expect an error from Knex, but instead it generates the following query:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="nv">`id`</span><span class="p">,</span> <span class="nv">`name`</span> <span class="k">FROM</span> <span class="nv">`users`</span> <span class="k">WHERE</span> <span class="nv">`id`</span> <span class="o">=</span> <span class="nv">`name`</span> <span class="o">=</span> <span class="s1">'foo'</span>
</code></pre></div></div>
<p>This query is not invalid. I don’t fully understand fully understand MySQL’s behavior,
but it causes the WHERE clause to be ignored and the result is equivalent to:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="nv">`id`</span><span class="p">,</span> <span class="nv">`name`</span> <span class="k">FROM</span> <span class="nv">`users`</span>
</code></pre></div></div>
<p>I think it has something to do with how MySQL casts things. In MySQL this yields <code class="language-plaintext highlighter-rouge">true</code> (or <code class="language-plaintext highlighter-rouge">1</code>):</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="mi">1</span> <span class="o">=</span> <span class="s1">'foo'</span> <span class="o">=</span> <span class="s1">'bar'</span>
</code></pre></div></div>
<h2 id="query-strings-and-express">Query strings and Express</h2>
<p>One place where this is especially scary, is if <code class="language-plaintext highlighter-rouge">lookupId</code> was provided by a user,
via a JSON body or query string.</p>
<p>Consider this example from an Express route:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">app</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">knex</span><span class="p">(</span><span class="dl">'</span><span class="s1">users</span><span class="dl">'</span><span class="p">)</span>
<span class="p">.</span><span class="nx">select</span><span class="p">([</span><span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">])</span>
<span class="p">.</span><span class="nx">where</span><span class="p">({</span><span class="na">id</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">query</span><span class="p">.</span><span class="nx">id</span><span class="p">});</span>
<span class="nx">res</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">result</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>
<p>Here the <code class="language-plaintext highlighter-rouge">id</code> in the <code class="language-plaintext highlighter-rouge">WHERE</code> clause is provided by the URL query string:</p>
<p>If a the server is opened with something like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost:3000/?id=2
</code></pre></div></div>
<p>It will return a single user, but if it was opened using:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://localhost:3000/?id%5Bname%5D=foo
</code></pre></div></div>
<p>It will return <em>every user</em>. And this issue is not limited to <code class="language-plaintext highlighter-rouge">SELECT</code>,
it could also trigger in a <code class="language-plaintext highlighter-rouge">WHERE</code> clause in <code class="language-plaintext highlighter-rouge">DELETE</code>.</p>
<p>The reason this works is that by crafting URL query parameters in a special
way, a user can have things in <code class="language-plaintext highlighter-rouge">req.query</code> show up as objects or arrays.</p>
<p>Express calls this the ‘extended’ syntax and turns it on by default. In my
opinion this is a bad default because it’s not really what users expect will
happen. PHP does this as well, and I believe this may have been where Express
(or specifically the <a href="https://www.npmjs.com/package/qs">qs</a> library) got the syntax from.</p>
<p>This is why I think Express users are expecially likely to be vulnerable.
I think that most developers in professional settings are decent at
validating JSON request bodies with a variety of tools, but I’ve noticed
this is often not the case for query parameters. I believe a big reason for
this is that the ‘extended’ syntax is not well known and ‘surprising’ behavior.
Many people assume these are just strings. This is not intended as a plug,
but this is also why the default behavior in our <a href="https://curveballjs.org/">Curveball framework</a> is
to not support this. In most cases it’s not needed, and if you do want it you can
use <code class="language-plaintext highlighter-rouge">qs</code> and explicitly opt-in.</p>
<h2 id="other-examples">Other examples</h2>
<p>But of course, this issue is not limited to query strings. If you’re not
validating input somewhere and this ends up in a <code class="language-plaintext highlighter-rouge">.where()</code> statement there’s
risk.</p>
<p>A specific example is a situation where part of the contents of your <code class="language-plaintext highlighter-rouge">WHERE</code>
clause is unguessable.</p>
<p>For instance, say you had a database table with records that users can only
see if they know a secret code, and it’s selected with:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">coupons</span> <span class="k">WHERE</span> <span class="n">product_id</span> <span class="o">=</span> <span class="mi">5</span> <span class="k">AND</span> <span class="n">coupon_code</span> <span class="o">=</span> <span class="s1">'BIG_OOF2023'</span>
</code></pre></div></div>
<p>A user presumably would have to <em>know</em> the <code class="language-plaintext highlighter-rouge">coupon_code</code> to see the coupons,
but if there’s no code to validate <code class="language-plaintext highlighter-rouge">coupon_code</code> is a string, a user would have the
power to change the query to this:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">coupons</span> <span class="k">WHERE</span> <span class="n">product_id</span> <span class="o">=</span> <span class="mi">5</span> <span class="k">AND</span> <span class="n">coupon_code</span> <span class="o">=</span> <span class="n">product_id</span> <span class="o">=</span> <span class="s1">'bla'</span>
</code></pre></div></div>
<p>Which is equivalent to:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">coupons</span> <span class="k">WHERE</span> <span class="n">product_id</span> <span class="o">=</span> <span class="mi">5</span>
</code></pre></div></div>
<p>And thus no longer requiring coupon codes to get ALL the discounts.</p>
<p>Lastly, if you can rewrite a query that expects 1 result to return every
record in a database is also an easy way to cause a <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack">Denial of service</a> attack.</p>
<p>This does demonstrate again how is critical validating is and throw errors
whenever you get data you don’t expect. Even if under normal circumstances
nothing weird can happen, a library you use might do the wrong thing with
unexpected data instead of just rejecting it.</p>
<h2 id="on-knex">On Knex</h2>
<p>It’s quite unfortunate to see that this went unpatched for so long. I’d
invite anyone reading this to try to not pile on on the (likely volunteer)
authors but think about the larger ecosystem.</p>
<p>This bug was hidden in plain sight. Lots of people must have randomly
ran into it and a ticket was opened. Probably the most responsible thing to
do would have been what <a href="https://github.com/knex/knex/issues/1227#issuecomment-1358165470">@rgmz</a> did: do their best to contact the authors,
failing that contact Github Security Team. After the Github Security Team also
weren’t able to connect to the authors, make a CVE which puts this on every
everyone’s radar. This ultimately led to a random bystander make their first
contribution and <a href="https://github.com/knex/knex/pull/5417">submit a fix</a>.</p>
<p>Knex feels high risk now though, and I can’t help wondering what else might
be unpatched. I’ve only recently made the jump from just writing my own queries
for like 20 years to Knex, but I’m probably reversing that decision for my
next project.</p>
http://evertpot.com/json5/I wish JSON5 was more popular2023-01-09T21:29:36+00:00Evert Pot[email protected]<p>As developers we write a lot of code, but we also deal with a lot of
configuration files.</p>
<p>The three major formats I tend to use day to day are:</p>
<ul>
<li>JSON</li>
<li>YAML</li>
<li>.env</li>
</ul>
<p>And, they all kinda suck. JSON feels like it should
<em>never</em> have become a format that people hand-write. So many quotes, and
and configuration files <strong>need comments</strong> to tell users <em>why</em> certain decisions
were made. <code class="language-plaintext highlighter-rouge">.env</code> has a specific purpose (and it’s ok at that), but it’s not a
great universal format, and YAML has always been difficult to read and write to me.
I can somehow never retain the syntax and end up copy-pasting things from examples.</p>
<h2 id="why-yaml-is-difficult-for-me">Why YAML is difficult for me</h2>
<p>A small example from Github workflows/actions:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v2</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-node@v2</span>
<span class="na">with</span><span class="pi">:</span>
<span class="na">node-version</span><span class="pi">:</span> <span class="m">14</span>
<span class="na">registry-url</span><span class="pi">:</span> <span class="s">https://registry.npmjs.org/</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm ci</span>
<span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm publish</span>
</code></pre></div></div>
<p>I couldn’t tell you why <code class="language-plaintext highlighter-rouge">uses</code> has a dash in front, and <code class="language-plaintext highlighter-rouge">node-version</code> does
not. If there’s a difference in how a YAML reader outputs them, I’m not sure
how I would be able to retain this while writing YAML.</p>
<p>I also use/love <a href="https://www.home-assistant.io/">home assistant</a>, which lets you write some pretty cool
automations using YAML. I wanted to play with
this but it’s been a barrier I’ve not been able to overcome. I don’t know
if it’s me. I’m been working as a programmer for 22 years. I’m decent at it,
but when when I chat with some of my peers (hi mhum!) they did not share my
sentiment.</p>
<p>YAML can also have very <a href="https://www.infoworld.com/article/3669238/7-yaml-gotchas-to-avoidand-how-to-avoid-them.html">surprising behavior</a>, with casting types:</p>
<p>From the <a href="https://www.infoworld.com/article/3669238/7-yaml-gotchas-to-avoidand-how-to-avoid-them.html">linked article</a>, this:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">country1</span><span class="pi">:</span> <span class="s">ca</span>
<span class="pi">-</span> <span class="na">country2</span><span class="pi">:</span> <span class="s">no</span>
</code></pre></div></div>
<p>Becomes:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">country1</span><span class="pi">:</span> <span class="s">ca</span>
<span class="pi">-</span> <span class="na">country2</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>
<p>It’s a bit cherry picked, and I’m sure there’s YAML linters out there that
help avoid the pitfalls, but in my mind configuration files should be simple.</p>
<p>There’s some configuration formats I like, such as <a href="https://toml.io/en/">TOML</a> and <a href="https://json5.org/">JSON5</a>.
They strike the right balance to me with being easy to read and
write, unambigious, supporting comments, strictness and not being incredibly
hard to write a parser for.</p>
<p>TOML is like ini files on steroids, and JSON5 is JSON but with fewer quotes,
comments and multi-line strings.</p>
<p>I <em>could</em> write my NPM configuration file as <code class="language-plaintext highlighter-rouge">package.json5</code> and automatically
convert it to <code class="language-plaintext highlighter-rouge">package.json</code> but that feels too surprising. My projects are
already kind of eclectic, so I want the ‘plumbing’ to be unsurprising. Plus
there’s the whole chicken and egg thing with needing a JSON5 parser before we
have dependencies.</p>
<p>I’d love the NPM project to adopt JSON5. It seems like a great fit. JSON and
YAML can’t be the final word for human-maintained data formats. It’s so
obviously sub-optimal.</p>
<p>If NPM adopted JSON5, I would annotate so much in my <code class="language-plaintext highlighter-rouge">package.json</code>. I’d
document why a dependency is needed, why we are stuck using a previous major
version of a dependency and what the purpose is of each script.</p>
<p>I wouldn’t know what format would be ideal for Github Actions. Maybe the
answer is ‘nothing’ and they need a good DSL.</p>
<p>And while we’re at it, stop polluting my projects root directory! Can’t we
all agree on a <code class="language-plaintext highlighter-rouge">.meta</code> directory for finding configuration files?</p>
http://evertpot.com/neko/Neko - A brief history and porting to Javascript2022-11-06T21:50:00+00:00Evert Pot[email protected]<p><span id="neko1"></span></p>
<p>In the early 90’s, being a <a href="https://en.wikipedia.org/wiki/Frisia">frisian</a> kid obsessed with computers there weren’t a
ton of ways to get access to new software or learn more about computers.</p>
<p>The two main ways were exchanging 3.5” diskettes with friends, or go to the
library. One book I remember more than others was “Windows for Kinderen”
(“Windows for Kids”) by Addo Stuur.</p>
<p>I must have been around 10 years old and was obsessed by this book. It covered
Windows 3.0, and when you got the book from the library, it came with a diskette
filled to the brim with shareware. Mostly games and
useless toys, but it still baffles me thinking they were able to cram it all on
a 1.44 megabyte disk. Using floppys from the libraries was even back then a
risky business given that they’re writable! Luckily this mostly went ok.</p>
<p>One that I remembered particularly well was ‘Neko’, an application that
renders a cat in a window that follows your mouse. This must have been a
popular thing to make at the time, because the diskette somehow had space
for 3(!) different ports of this same application.</p>
<p>I don’t know what reminded me of Neko last week, but I started doing some
more research, and found out that the first version was written all the
way back in the 1980’s by Naoshi Watanabe for the <a href="https://en.wikipedia.org/wiki/PC-9800_series">NEC PC 9801</a>.</p>
<figure>
<img src="/assets/posts/neko/neko-pc9801.gif" alt="Neko for the NEC PC 9801" style="width: 320px; max-width: 100%; image-rendering: crisp-edges; image-rendering: pixelated" /><br />
<figcaption>Neko for the NEC PC 9801 (1980's)</figcaption>
</figure>
<p>After that, it was ported to the Macintosh in 1989 by Kenji Gotoh, and this
art style seems to be the basis of almost every future port:</p>
<figure>
<img src="/assets/posts/neko/neko-macintosh.png" alt="Neko on Macintosh" style="width: 584px; max-width: 100%; image-rendering: crisp-edges; image-rendering: pixelated" />
<figcaption>Neko on Macintosh (1989)</figcaption>
</figure>
<p>In 1991, IBM included it in OS/2! Apparently they paid 300,000 YEN, which
is about $3000 CAD in todays money. At this point it also became public
domain.</p>
<figure>
<img src="/assets/posts/neko/neko-os2.png" alt="Neko on OS/2" style="max-width: 100%" />
<figcaption>Neko on OS/2 (1991)</figcaption>
</figure>
<p>Since then there’s been countless ports for every platform. If you’re running
linux you might be able to install one by just running:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt <span class="nb">install </span>oneko
</code></pre></div></div>
<p>I also decided to <a href="https://github.com/evert/jneko" title="jneko source">make a version</a>. Neko is now close to 40, so my Neko is
no longer interested in following the mouse, and prefers to just sleep all day
unless you wake it.</p>
<div id="neko2"></div>
<p>If you want to know more, the Eliot Akira wrote <a href="https://eliotakira.com/neko/" title="Neko: History of a Software Pet">a great article with way
more information about Neko and all its ports</a>,
this is also where I took the screenshots from this page.</p>
<p>You can also check out the <a href="https://github.com/evert/jneko" title="jneko source">source</a> of my port ‘jneko’. It uses the assets
of <a href="https://github.com/tie/oneko" title="ONeko source">oneko</a>, which are public domain. It was a fun exercise to create a
little state machine.</p>
<h2 id="creating-the-assets">Creating the assets</h2>
<p>To create the assets for <a href="https://github.com/evert/jneko" title="jneko source">jneko</a>, I had to convert them from the <code class="language-plaintext highlighter-rouge">.xbm</code>
format from <a href="https://github.com/tie/oneko" title="ONeko source">oneko</a>.</p>
<p>I did this with <code class="language-plaintext highlighter-rouge">imagemagick</code> on the command line using a command like this
to loop through all the files:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for </span>f <span class="k">in</span> ../oneko/bitmaps/neko/<span class="k">*</span>.xbm
convert <span class="s2">"</span><span class="nv">$f</span><span class="s2">"</span> <span class="sb">`</span><span class="nb">basename</span> <span class="s2">"</span><span class="k">${</span><span class="nv">f</span><span class="p">%.xbm</span><span class="k">}</span><span class="s2">.png"</span><span class="sb">`</span>
</code></pre></div></div>
<p>Sadly I still had to open each one of them with Gimp and add the alpha layer.</p>
<p>I hadn’t really heard of the <code class="language-plaintext highlighter-rouge">.xbm</code> format, but when I accidentally opened
one with an editor I was surprised to see that they’re actually a text
format:</p>
<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#define awake_width 32
#define awake_height 32
</span><span class="k">static</span> <span class="kt">char</span> <span class="n">awake_bits</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x20</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x04</span><span class="p">,</span>
<span class="mh">0x40</span><span class="p">,</span> <span class="mh">0x10</span><span class="p">,</span> <span class="mh">0x10</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span> <span class="mh">0x80</span><span class="p">,</span> <span class="mh">0x28</span><span class="p">,</span> <span class="mh">0x28</span><span class="p">,</span> <span class="mh">0x01</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x49</span><span class="p">,</span> <span class="mh">0x24</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span>
<span class="mh">0x06</span><span class="p">,</span> <span class="mh">0x44</span><span class="p">,</span> <span class="mh">0x44</span><span class="p">,</span> <span class="mh">0x60</span><span class="p">,</span> <span class="mh">0x18</span><span class="p">,</span> <span class="mh">0x84</span><span class="p">,</span> <span class="mh">0x42</span><span class="p">,</span> <span class="mh">0x18</span><span class="p">,</span> <span class="mh">0x60</span><span class="p">,</span> <span class="mh">0x82</span><span class="p">,</span> <span class="mh">0x83</span><span class="p">,</span> <span class="mh">0x06</span><span class="p">,</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span> <span class="mh">0x80</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x22</span><span class="p">,</span> <span class="mh">0x88</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x0f</span><span class="p">,</span> <span class="mh">0x22</span><span class="p">,</span> <span class="mh">0x88</span><span class="p">,</span> <span class="mh">0x78</span><span class="p">,</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x22</span><span class="p">,</span> <span class="mh">0x88</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span> <span class="mh">0x80</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x3a</span><span class="p">,</span> <span class="mh">0xb9</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x04</span><span class="p">,</span> <span class="mh">0x40</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x08</span><span class="p">,</span> <span class="mh">0x20</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x70</span><span class="p">,</span> <span class="mh">0x1c</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x40</span><span class="p">,</span> <span class="mh">0x04</span><span class="p">,</span> <span class="mh">0x05</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x20</span><span class="p">,</span> <span class="mh">0x88</span><span class="p">,</span> <span class="mh">0x04</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x10</span><span class="p">,</span> <span class="mh">0x50</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x08</span><span class="p">,</span> <span class="mh">0x20</span><span class="p">,</span> <span class="mh">0x01</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x0b</span><span class="p">,</span> <span class="mh">0xa0</span><span class="p">,</span> <span class="mh">0x01</span><span class="p">,</span> <span class="mh">0x80</span><span class="p">,</span> <span class="mh">0x0c</span><span class="p">,</span> <span class="mh">0x61</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span>
<span class="mh">0x40</span><span class="p">,</span> <span class="mh">0x18</span><span class="p">,</span> <span class="mh">0x31</span><span class="p">,</span> <span class="mh">0x04</span><span class="p">,</span> <span class="mh">0x40</span><span class="p">,</span> <span class="mh">0x10</span><span class="p">,</span> <span class="mh">0x11</span><span class="p">,</span> <span class="mh">0x04</span><span class="p">,</span> <span class="mh">0xc0</span><span class="p">,</span> <span class="mh">0x11</span><span class="p">,</span> <span class="mh">0x11</span><span class="p">,</span> <span class="mh">0x07</span><span class="p">,</span>
<span class="mh">0x60</span><span class="p">,</span> <span class="mh">0x90</span><span class="p">,</span> <span class="mh">0x13</span><span class="p">,</span> <span class="mh">0x0c</span><span class="p">,</span> <span class="mh">0xe0</span><span class="p">,</span> <span class="mh">0xff</span><span class="p">,</span> <span class="mh">0xfe</span><span class="p">,</span> <span class="mh">0x0f</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span>
<span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">};</span>
</code></pre></div></div>
<p>Turns out this is the <a href="https://en.wikipedia.org/wiki/X_BitMap" title="X BitMap file format">X BitMap</a> format, which has the interesting property
that it aside from an image format, they’re also valid C source files.
This has the advantage that they can easily be statically compiled into C files without
requiring a parser.</p>
<p>This is really similar to the history of the JSON format, which was designed
to be a subset of Javascript, allowing Javascript programs to read the
format without a new parser (using <code class="language-plaintext highlighter-rouge">eval</code>). Ironically the current JSON format
is no longer a subset of Javascript, and Javascript engines now have a
<code class="language-plaintext highlighter-rouge">JSON.parse()</code> function built in.</p>
<h2 id="a-deep-dive-into-the-source">A deep dive into the source</h2>
<p>jneko uses a <a href="https://en.wikipedia.org/wiki/Finite-state_machine">state machine</a>. All its possible states and transitions are
expressed with this configuration object:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">stateMachine</span> <span class="o">=</span> <span class="p">{</span>
<span class="c1">// Name of the state</span>
<span class="na">sleep</span><span class="p">:</span> <span class="p">{</span>
<span class="c1">// Which images are used for the state</span>
<span class="na">image</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">sleep1</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">sleep2</span><span class="dl">'</span><span class="p">],</span>
<span class="c1">// How quickly we loop through these images</span>
<span class="na">imageInterval</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="c1">// What state does this go to when clicked</span>
<span class="na">click</span><span class="p">:</span> <span class="dl">'</span><span class="s1">awake</span><span class="dl">'</span>
<span class="p">},</span>
<span class="na">awake</span><span class="p">:</span> <span class="p">{</span>
<span class="na">image</span><span class="p">:</span> <span class="dl">'</span><span class="s1">awake</span><span class="dl">'</span><span class="p">,</span>
<span class="c1">// We automaticall transition to this state</span>
<span class="na">nextState</span><span class="p">:</span> <span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span>
<span class="c1">// How long it takes to transition</span>
<span class="na">nextStateDelay</span><span class="p">:</span> <span class="mf">2.5</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">normal</span><span class="p">:</span> <span class="p">{</span>
<span class="na">image</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mati2</span><span class="dl">'</span><span class="p">,</span>
<span class="c1">// If there's multiple nextState values, a random one is picked.</span>
<span class="c1">// You make make 1 state more likely by addding it multiple times</span>
<span class="c1">// to the array</span>
<span class="na">nextState</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">tilt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">scratch</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">yawn</span><span class="dl">'</span><span class="p">],</span>
<span class="na">nextStateDelay</span><span class="p">:</span> <span class="mf">1.5</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">tilt</span><span class="p">:</span> <span class="p">{</span>
<span class="na">image</span><span class="p">:</span> <span class="dl">'</span><span class="s1">jare2</span><span class="dl">'</span><span class="p">,</span>
<span class="na">nextState</span><span class="p">:</span> <span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span>
<span class="na">nextStateDelay</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">yawn</span><span class="p">:</span> <span class="p">{</span>
<span class="na">image</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mati3</span><span class="dl">'</span><span class="p">,</span>
<span class="na">nextState</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">sleep</span><span class="dl">'</span><span class="p">],</span>
<span class="na">nextStateDelay</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="p">},</span>
<span class="na">scratch</span><span class="p">:</span> <span class="p">{</span>
<span class="na">image</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">kaki1</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">kaki2</span><span class="dl">'</span><span class="p">],</span>
<span class="na">imageInterval</span><span class="p">:</span> <span class="mf">0.1</span><span class="p">,</span>
<span class="na">nextState</span><span class="p">:</span> <span class="dl">'</span><span class="s1">normal</span><span class="dl">'</span><span class="p">,</span>
<span class="na">nextStateDelay</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>
<p>Before Neko starts, we need to preload the images. This ensures that the
animations happen instantly and not after a delay.</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* The filenames are a bit funny because they were taken straight from
* the oneko project.
*/</span>
<span class="kd">const</span> <span class="nx">imageNames</span> <span class="o">=</span> <span class="p">[</span>
<span class="dl">'</span><span class="s1">awake</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">jare2</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">kaki1</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">kaki2</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">mati2</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">mati3</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">sleep1</span><span class="dl">'</span><span class="p">,</span>
<span class="dl">'</span><span class="s1">sleep2</span><span class="dl">'</span><span class="p">,</span>
<span class="p">];</span>
<span class="cm">/**
* Images are actually 32x32 but it's too small for modern screens.
*/</span>
<span class="kd">const</span> <span class="nx">nekoSize</span> <span class="o">=</span> <span class="mi">64</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">images</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">fromEntries</span><span class="p">(</span><span class="nx">imageNames</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">name</span> <span class="o">=></span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">image</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">(</span><span class="nx">nekoSize</span><span class="p">,</span> <span class="nx">nekoSize</span><span class="p">);</span>
<span class="nx">image</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">/assets/posts/neko/bitmaps/</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">name</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">.png</span><span class="dl">'</span><span class="p">;</span>
<span class="k">return</span> <span class="p">[</span><span class="nx">name</span><span class="p">,</span> <span class="nx">image</span><span class="p">]</span>
<span class="p">}));</span>
</code></pre></div></div>
<p>Finally, this class does all the heavy lifting:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">Neko</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">elem</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// The HTML element that hosts neko</span>
<span class="k">this</span><span class="p">.</span><span class="nx">elem</span> <span class="o">=</span> <span class="nx">elem</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="nx">stateMachine</span> <span class="o">=</span> <span class="nx">stateMachine</span><span class="p">;</span>
<span class="c1">// Creating a new <img> element</span>
<span class="k">this</span><span class="p">.</span><span class="nx">imgElem</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Image</span><span class="p">(</span><span class="nx">nekoSize</span><span class="p">,</span> <span class="nx">nekoSize</span><span class="p">);</span>
<span class="nx">elem</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">imgElem</span><span class="p">);</span>
<span class="k">this</span><span class="p">.</span><span class="nx">imgElem</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">click</span><span class="dl">'</span><span class="p">,</span> <span class="p">()</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">onClick</span><span class="p">());</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">(</span><span class="dl">'</span><span class="s1">sleep</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
<span class="cm">/**
* This property is used to keep track of states with multiple frames
*/</span>
<span class="err">#</span><span class="nx">animationIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="nx">renderImage</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">let</span> <span class="nx">name</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">stateMachine</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">state</span><span class="p">].</span><span class="nx">image</span><span class="p">;</span>
<span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">animationIndex</span><span class="o">++</span><span class="p">;</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">name</span> <span class="o">=</span> <span class="nx">name</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">animationIndex</span> <span class="o">%</span> <span class="nx">name</span><span class="p">.</span><span class="nx">length</span><span class="p">];</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="nx">imgElem</span><span class="p">.</span><span class="nx">src</span> <span class="o">=</span> <span class="nx">images</span><span class="p">[</span><span class="nx">name</span><span class="p">].</span><span class="nx">src</span><span class="p">;</span>
<span class="p">}</span>
<span class="err">#</span><span class="nx">state</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="err">#</span><span class="nx">nextStateTimeout</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="err">#</span><span class="nx">imageCycleInterval</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="nx">setState</span><span class="p">(</span><span class="nx">stateName</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">clearTimeout</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">nextStateTimeout</span><span class="p">);</span>
<span class="nx">clearInterval</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">imageCycleInterval</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">stateName</span><span class="p">))</span> <span class="p">{</span>
<span class="c1">// If stateName was supplied as an array of strings, we'll randomly</span>
<span class="c1">// pick a new state.</span>
<span class="nx">stateName</span> <span class="o">=</span> <span class="nx">stateName</span><span class="p">[</span><span class="nb">Math</span><span class="p">.</span><span class="nx">floor</span><span class="p">(</span><span class="nb">Math</span><span class="p">.</span><span class="nx">random</span><span class="p">()</span><span class="o">*</span><span class="p">(</span><span class="nx">stateName</span><span class="p">.</span><span class="nx">length</span><span class="p">))];</span>
<span class="p">}</span>
<span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">stateMachine</span><span class="p">[</span><span class="nx">stateName</span><span class="p">])</span> <span class="p">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Uknown state: </span><span class="dl">'</span> <span class="o">+</span> <span class="nx">stateName</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">state</span> <span class="o">=</span> <span class="nx">stateName</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">stateData</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">stateMachine</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">state</span><span class="p">];</span>
<span class="c1">// If there was a nextState, we automatically transition there after</span>
<span class="c1">// a delay.</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">stateData</span><span class="p">.</span><span class="nx">nextState</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">nextStateTimeout</span> <span class="o">=</span> <span class="nx">setTimeout</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">(</span><span class="nx">stateData</span><span class="p">.</span><span class="nx">nextState</span><span class="p">),</span>
<span class="nx">stateData</span><span class="p">.</span><span class="nx">nextStateDelay</span> <span class="o">*</span> <span class="mi">1000</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="c1">// This block is responsible for cycling through multiple images</span>
<span class="c1">// of the current state.</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">stateData</span><span class="p">.</span><span class="nx">imageInterval</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">imageCycleInterval</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(</span>
<span class="p">()</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">renderImage</span><span class="p">(),</span>
<span class="nx">stateData</span><span class="p">.</span><span class="nx">imageInterval</span><span class="o">*</span><span class="mi">1000</span>
<span class="p">);</span>
<span class="p">}</span>
<span class="k">this</span><span class="p">.</span><span class="nx">renderImage</span><span class="p">();</span>
<span class="p">}</span>
<span class="nx">onClick</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">stateData</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">stateMachine</span><span class="p">[</span><span class="k">this</span><span class="p">.</span><span class="err">#</span><span class="nx">state</span><span class="p">];</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">stateData</span><span class="p">.</span><span class="nx">click</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// If the current state had a 'click' property, we'll transition</span>
<span class="c1">// to that state, otherwise it's ignored.</span>
<span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">(</span><span class="nx">stateData</span><span class="p">.</span><span class="nx">click</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Now if you’d like to call Neko, make a <code class="language-plaintext highlighter-rouge"><div id="neko"></div></code> somewhere. and
hook up Neko with:</p>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Neko</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">neko</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div></div>
<p>To make Neko not look ugly, tell the browser to not use anti-aliasing when
scaling:</p>
<div class="language-css highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">#neko</span> <span class="nt">img</span> <span class="p">{</span>
<span class="c">/* Both are needed to support all browsers currently. */</span>
<span class="nl">image-rendering</span><span class="p">:</span> <span class="n">pixelated</span><span class="p">;</span>
<span class="nl">image-rendering</span><span class="p">:</span> <span class="n">crisp-edges</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Thanks for reading! If you have any comments you can reply to <a href="https://twitter.com/evertp/status/1589376231157690368">this tweet</a>
or <a href="https://indieweb.social/web/@evert/109299079542445353">this mastadon post</a> to make it show up below this article automatically.</p>
<div id="neko3"></div>
<script src="/assets/posts/neko/jneko.js"></script>
<script>
function loadNeko() {
new Neko(document.getElementById('neko1'));
new Neko(document.getElementById('neko2'));
new Neko(document.getElementById('neko3'));
}
window.addEventListener('DOMContentLoaded', () => loadNeko());
</script>
http://evertpot.com/hello-mastodon/Taking a look at Mastodon2022-11-01T19:35:00+00:00Evert Pot[email protected]<p>I’ve been a Twitter user and fan since 2007. With Twitter’s future
looking a bit grim, I started looking around if there’s another place to go.</p>
<p>Twitter can’t really be replaced with anything else, because everyone’s
Twitter experience is unique to them and their community. For me, it’s
the main way I stay in touch with my Open Source / HTTP / API / Hypermedia
bubble and some friends. Losing that would suck! Unfortunately, there’s no way
that group would all flock to another platform.</p>
<p>But for the ones that do try something else, the talk of the town seems
to be <a href="https://joinmastodon.org/">Mastodon</a>.</p>
<p>Mastodon is interesting. On the surface it might just seem like a
Twitter clone, but it’s based on a federated protocol called ‘ActivityPub’.
What this means in practice is that there’s no central server. There’s
many <a href="https://joinmastodon.org/servers">instances</a>. Each of these instances
is managed by different people, and many of them focus on specific interests.</p>
<p>With email, it doesn’t matter which provider you go with. Thanks to universal
SMTP standards that every server uses, you can exchange messages with everyone
else. This is the same with Mastodon. You’re not siloed into a single instance,
and you can follow people from any other instance. Unlike email, it appears
that with Mastodon you can actually migrate to different instances if you don’t
like your current one.</p>
<p>This has some interesting side effects too. I joined the
<a href="https://indieweb.org/">IndieWeb</a> instance, which is a community I already
loved. And even though I’m not siloed in, I get access to a local feed of
like-minded people from that community. Everything feels new and more
intimate.</p>
<p>Also, instead of one central authority that you have to trust to make the right
moderation decisions, you can join one of many that aligns with your values,
and you can block entire instances that don’t.</p>
<p>So should you join? If you use Twitter to stay on top of news and follow high
profile people then probably not. If you’re like me, you might be able to
find a community that fits your interest.</p>
<p>Will I stick to this? Who knows… but Twitter, like everything before,
will fall out of favor one day and I’m enjoying Mastodon’s ad-free, open source,
developer-friendly experience. Reminds me of early Twitter or mid-2000’s
blogging culture.</p>
<h2 id="links">Links</h2>
<ul>
<li>If you’re just getting started, check out <a href="https://joinmastodon.org/">https://joinmastodon.org/</a> and
find a server.</li>
<li><a href="https://indieweb.social/web/@evert">Follow me!</a></li>
<li>To find your Twitter friends on Mastodon try <a href="https://twitodon.com/">Twitodon</a> and <a href="https://fedifinder.glitch.me/">Fedifinder</a>.</li>
<li>To automatically cross-post everything from Twitter to Mastodon and vice versa, use <a href="https://moa.party/">Moa</a>.</li>
</ul>
<p>Lastly, one of the interesting results of Mastodon building on open protocols,
is that it allows alternative implementations.</p>
<p>The <a href="https://microblog.pub/">Microblog.pub</a> project lets you self-host a
single-user instance. Instead of joining some large instance, you deploy
an instance on your own domain that’s just for you. Can’t get more control
than that, and this might be something I’ll consider in the future.</p>
<p>I don’t see why this blog couldn’t one day also be a ‘microblog’ and part of the
<a href="https://en.wikipedia.org/wiki/Fediverse">fediverse</a>.</p>
http://evertpot.com/bun-curveball-framework/Porting Curveball to Bun2022-09-13T16:30:00+00:00Evert Pot[email protected]<p><a href="https://bun.sh/">Bun</a> is the hot new server-side Javascript runtime, in the same category
as <a href="https://nodejs.org/">Node</a> and <a href="https://deno.land/">Deno</a>. Bun uses the <a href="https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore">JavascriptCore</a> engine from
Webkit, unlike Node and Deno which use <a href="https://v8.dev/">V8</a>. A big selling point is that
it’s coming out faster in a many benchmarks, however the things I’m personally
excited about is some of it’s quality of life features:</p>
<ul>
<li>It parses Typescript and JSX by default (but doesn’t type check), which
means there’s no need for a separate ‘dist’ directory, or a separate tool
like <code class="language-plaintext highlighter-rouge">ts-node</code>.</li>
<li>It loads <code class="language-plaintext highlighter-rouge">.env</code> files by default.</li>
<li>It’s compatible with NPM, <code class="language-plaintext highlighter-rouge">package.json</code>, and many built-in Node modules.</li>
</ul>
<p>I also like that it’s ‘Hello world HTTP server’ is as simple as writing this
file:</p>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// http.ts</span>
<span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
<span class="na">port</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
<span class="nx">fetch</span><span class="p">(</span><span class="na">request</span><span class="p">:</span> <span class="nx">Request</span><span class="p">):</span> <span class="nb">Promise</span><span class="o"><</span><span class="nx">Response</span><span class="o">></span> <span class="p">{</span>
<span class="k">return</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">"</span><span class="s2">Hello world!</span><span class="dl">"</span><span class="p">);</span>
<span class="p">},</span>
<span class="p">};</span>
</code></pre></div></div>
<p>And then running it with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bun run http.ts
</code></pre></div></div>
<p>Bun will recognize that an object with a <code class="language-plaintext highlighter-rouge">fetch</code> function was default-exported,
and start a server on port 3000. As you can see here, this uses the standard
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Request">Request</a> and <a href="https://github.com/curveball/aws-lambda">Response</a> objects you use in a browser, and can use
async/await.</p>
<p>These are all things that didn’t exist when Node and Express were first
created, but seem like pretty good ideas for something built today. I don’t think
using <code class="language-plaintext highlighter-rouge">Request</code> and <code class="language-plaintext highlighter-rouge">Response</code> are good for more complex use-cases (streaming
responses, 1xx responses, trailers, upgrading to other protocols, getting tcp
connection metadata like remoteAddr are some that come to mind),
because these objects are designed for clients first.</p>
<p>But in many cases people are just building simple endpoints, and for that it’s
great.</p>
<p>Bun supports a ton of the standard Node modules, but it’s also missing some
such as support for server-side websockets and the node http/https/https
packages, which for now makes it incompatible with popular frameworks like
Express.</p>
<h2 id="porting-curveball">Porting Curveball</h2>
<p><a href="https://curveballjs.org/">Curveball</a> is a Typescript micro-framework we’ve been developing since
mid-2018 as a modern replacement for Express and Koa. A key difference between
Curveball and these two frameworks is that it fully abstracts and encapsulates
the core ‘Request’ and ‘Response’ objects Node provides.</p>
<p>This made it very easy to create a lambda integration in the past; instead of
mapping to Node’s Request and Response types, All I needed was simple mapping
function for Lambdas idea of what a request and response looks like.</p>
<p>To get Express to run on AWS Lambda the Node <code class="language-plaintext highlighter-rouge">http</code> stack needs to be emulated, or
a full-blown HTTP/TCP server needs to be started and proxied to. Each of these
workarounds require a ton of code from libraries like <a href="https://github.com/vendia/serverless-express">serverless-express</a>.</p>
<p>So with Bun up and coming, either the same work would need to be done to emulate
Node’s APIs, or Bun would would need to add full compability for the Node <code class="language-plaintext highlighter-rouge">http</code>
module (which is eventually coming).</p>
<p>But because of Curveball’s message abstractions it was relatively easy to get
up and running. Most of the work was moving the Node-specific code into a new
package.</p>
<p>Here’s the Curveball ‘hello world’ on Bun:</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">Application</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@curveball/kernel</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Application</span><span class="p">();</span>
<span class="c1">// Add all your middlewares here! This should look familiar to Koa users.</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span> <span class="nx">ctx</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="p">{</span><span class="na">msg</span><span class="p">:</span> <span class="dl">'</span><span class="s1">hello world!</span><span class="dl">'</span><span class="p">};</span>
<span class="p">});</span>
<span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
<span class="na">port</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
<span class="na">fetch</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">fetch</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">app</span><span class="p">)</span>
<span class="p">};</span>
</code></pre></div></div>
<p>It’s still a bit experimental, but the following middlewares are tested:</p>
<ul>
<li><a href="https://github.com/curveball/router">router</a></li>
<li><a href="https://github.com/curveball/controller">controller</a></li>
<li><a href="https://github.com/curveball/cors">cors</a></li>
<li><a href="https://github.com/curveball/accesslog">accesslog</a></li>
<li><a href="https://github.com/curveball/bodyparser">bodyparser</a></li>
<li><a href="https://github.com/curveball/validator">validator</a></li>
</ul>
<p>And because JSX also just works in Bun, it’s relatively easy to use it to
generate HTML:</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">Application</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@curveball/kernel</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">reactMw</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@curveball/react</span><span class="dl">'</span><span class="p">;</span>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Application</span><span class="p">();</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span><span class="nx">reactMw</span><span class="p">());</span>
<span class="nx">app</span><span class="p">.</span><span class="nx">use</span><span class="p">(</span> <span class="nx">ctx</span> <span class="o">=></span> <span class="p">{</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="kd">type</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">text/html; charset=utf-8</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">ctx</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">body</span> <span class="o">=</span> <span class="o"><</span><span class="nx">html</span><span class="o">></span>
<span class="o"><</span><span class="nx">body</span><span class="o">></span>
<span class="o"><</span><span class="nx">div</span><span class="o">></span>
<span class="o"><</span><span class="nx">h1</span><span class="o">></span><span class="nx">Hello</span> <span class="nx">world</span><span class="o">!<</span><span class="sr">/h1</span><span class="err">>
</span> <span class="o"><</span><span class="sr">/div</span><span class="err">>
</span> <span class="o"><</span><span class="sr">/body</span><span class="err">>
</span> <span class="o"><</span><span class="sr">/html></span><span class="err">;
</span>
<span class="p">});</span>
<span class="k">export</span> <span class="k">default</span> <span class="p">{</span>
<span class="na">port</span><span class="p">:</span> <span class="mi">3000</span><span class="p">,</span>
<span class="na">fetch</span><span class="p">:</span> <span class="nx">app</span><span class="p">.</span><span class="nx">fetch</span><span class="p">.</span><span class="nx">bind</span><span class="p">(</span><span class="nx">app</span><span class="p">)</span>
<span class="p">};</span>
</code></pre></div></div>
<p>This is not a full-blown hydration/server-rendering solution, but JSX has
replaced template engines like EJS and Handlebars for us at
<a href="https://badgateway.net/">Bad Gateway</a>. JSX lets you do the same things, but you get the advantage
of type checking, it’s harder to create XSS bugs and full Javascript access.</p>
<h2 id="final-thoughts-on-bun">Final thoughts on Bun</h2>
<p>Compared to Deno, It was considerably easier to port Curveball to Bun.
Deno was a much greater challenge, due to it having such a radically
different idea about packages and module loading. This was over a year
ago, so it’s worth giving a shot again.</p>
<p>So, I’m curious where all of this goes! Perhaps Bun is the future, or
perhaps we’ll see Node get parity with Bun and making it obsolete. Either
way competition is good.</p>
<p>I feel Bun has a better chance than Deno, because it delivers many of
its advantages and features while mostly staying inside with the Node
ecosystem.</p>
<p>Although as it turns out Deno also has changed their tune and also made
it <a href="https://deno.com/blog/changes">a new goal</a> to be compatible. I can’t help wondering if this was
inspired by Bun’s recent success as well.</p>
http://evertpot.com/firefox-ubuntu-snap/Ubuntu bungled the Firefox Snap package transition2022-09-01T09:39:29+00:00Evert Pot[email protected]<p>I’m not a Snap hater. On paper it’s a good idea, but as a user I shouldn’t
really be aware that ‘snaps’ even exist. In Ubuntu 21.10, Firefox became
a snap package.</p>
<p>Arguably the browser is the most important application in an operating
system. Here’s a non-exhaustive list of issues I’ve personally ran into.
I should note that some of these issues are now fixed, but I wanted to
illustrate what Ubuntu launched with:</p>
<ul>
<li>Printing is completely broken, and I can only print to PDF.</li>
<li><a href="https://keepassxc.org/">KeePassXC</a>, an open source password managers’ browser extension no longer works.</li>
<li>Firefox thinks that when opening ‘localhost:8080’ should open the URI scheme ‘localhost’ and tries to
find an application that supports this scheme (now fixed!)</li>
<li><a href="https://extensions.gnome.org/local/">Gnome shell integration</a> extension, the primary way to install
gnome add-ons is now broken.</li>
<li>‘Set image as desktop background’ is broken.</li>
<li>Opening applications via a custom URI scheme no longer asks for confirmation, this makes it possible
to (for example) launch a bittorrent client like Transmission via a <code class="language-plaintext highlighter-rouge">magnet:</code> uri without asking a
user.</li>
<li>The <a href="https://www.mozilla.org/en-CA/products/vpn/">Mozilla VPN</a> product has a neat feature that lets
you have specific containers always use a VPN. This doesn’t work.</li>
<li>Firefox creates a ‘firefox.tmp’ directory in the Downloads folder (fixed!)</li>
<li>When there’s an update to the Firefox package, the following notification appears, once per day.
Restarting Firefox does not make this go away. The official answer is to run <code class="language-plaintext highlighter-rouge">snap refresh firefox</code> to
make it go away.</li>
<li><a href="https://github.com/GSConnect/gnome-shell-extension-gsconnect/wiki">GSConnect</a> Firefox Add-on is
broken.</li>
</ul>
<p><img src="/assets/posts/firefox-snap.png" title="Update Firefox To Avoid Disruptions notification in Ubuntu" /></p>
<p>This is just the stuff I ran into myself, (and I have reported most of these). I imagine the total list of bugs must be way higher.
I don’t usually go out and complain on the internet like this, especially when it’s about open source projects.
I’m a Linux user, so I’ve kind of come to expect things to not be quite as polished as some of its commercial
counterparts. They’re small trade-offs to support Open Source.</p>
<p>However, I’m so surprised by the lack of quality control for arguably the #1 application on the #1 linux distro I’m
frankly flabbergasted, and for the first time since switching from Debian to Ubuntu 15ish years ago I’m considering
<a href="https://getfedora.org/">jumping ship</a> again. What happened here?</p>
<p>Comments? Reply to <a href="https://twitter.com/evertp/status/1565819403245129728">this tweet</a></p>
http://evertpot.com/syntactic-sugar/On syntactic sugar2022-08-13T18:12:29+00:00Evert Pot[email protected]<p>Ever so often the term ‘syntactic sugar’ comes when people discuss language
features, and it’s not uncommon to see the word ‘just’ right in front of it;
some examples:</p>
<ul>
<li><a href="https://www.zhenghao.io/posts/await-vs-promise">Why Async/Await Is More Than Just Syntactic Sugar</a></li>
<li><a href="https://webreflection.medium.com/js-classes-are-not-just-syntactic-sugar-28690fedf078">JS classes are not “just syntactic sugar”</a></li>
</ul>
<p>The ‘just’ has a lot of meaning here. To me it suggests that language features
that are ‘just’ syntactic sugar, aren’t quite as important as features that
aren’t. Maybe it even suggests to me that the language would be fine without.</p>
<p>So while the above two examples both argue that both Javascript classes and
async/await <em>aren’t</em> syntactic sugar, they also kind of come to the defence
of those features and justify their existence. In other cases when people
call something syntactic sugar, it’s often in a context that’s somewhat
dismissal of the feature.</p>
<p>I think this is a bit odd and also <a href="https://dev.to/jenc/shtpost-can-we-stop-saying-syntactic-sugar-3i4j">confuses people</a>, especially since any
actual definition I’ve found is generally positive, such as this one from
<a href="https://en.wikipedia.org/wiki/Syntactic_sugar">Wikipedia</a>.</p>
<blockquote>
<p>In computer science, syntactic sugar is syntax within a programming language
that is designed to make things easier to read or to express. It makes the
language “sweeter” for human use: things can be expressed more clearly, more
concisely, or in an alternative style that some may prefer.</p>
</blockquote>
<p>The thing is, isn’t every language feature beyond the bare minimum of what
makes a language turing-complete syntax sugar?</p>
<ul>
<li>You don’t need classes because you can use functions and structs.</li>
<li>You don’t really need types because everything can fit in a string.</li>
<li>Functions can be implemented with goto.</li>
<li>Multiply can be implemented with addition.</li>
<li><code class="language-plaintext highlighter-rouge">or</code> and <code class="language-plaintext highlighter-rouge">and</code>, <code class="language-plaintext highlighter-rouge">xor</code> can be implemented with <code class="language-plaintext highlighter-rouge">nand</code>.</li>
<li>async/await can be implemented with generators.</li>
</ul>
<p>These are all incredibly useful features that make it easier to read and write
code and express ideas. Almost every language feature could be considered
syntactic sugar. That’s not a bad thing, it’s just uninteresting to point out.</p>
http://evertpot.com/oauth2-javascript-client/A new OAuth2 client for Javascript2022-06-20T19:40:00+00:00Evert Pot[email protected]<p>Frustrated with the lack of well maintained, minimal OAuth2 libraries, I <a href="https://github.com/badgateway/oauth2-client" title="OAuth2 Client on github">wrote
my own</a>. This new OAuth2 library is only <strong>3KB</strong> gzipped, mainly because it
has <strong>0</strong> dependencies and relies on modern APIs like <code class="language-plaintext highlighter-rouge">fetch()</code> and
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API" title="Web Crypto API">Web Crypto</a> which are built in Node 18 (but it works with Polyfills on
Node 14 and 16).</p>
<p>It has support for key features such as:</p>
<ul>
<li><code class="language-plaintext highlighter-rouge">authorization_code</code> with <a href="https://datatracker.ietf.org/doc/html/rfc7636" title="Proof Key for Code Exchange by OAuth Public Clients">PKCE</a> support.</li>
<li><code class="language-plaintext highlighter-rouge">password</code> and <code class="language-plaintext highlighter-rouge">client_credentials</code> grants.</li>
<li>a <code class="language-plaintext highlighter-rouge">fetch()</code> wrapper that automatically adds Bearer tokens and refreshes them.</li>
<li>OAuth2 endpoint discovery via the Server metadata document (<a href="https://datatracker.ietf.org/doc/html/rfc8414" title="OAuth 2.0 Authorization Server Metadata">RFC8414</a>).</li>
<li>OAuth2 Token Introspection (<a href="https://datatracker.ietf.org/doc/html/rfc7662" title="OAuth 2.0 Token Introspection">RFC7662</a>).</li>
</ul>
<p>If your server does support the meta-data document, here’s how simple the
process can be:</p>
<h2 id="client_credentials-example">client_credentials example</h2>
<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">OAuth2Client</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@badgateway/oauth2-client</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Client</span><span class="p">({</span>
<span class="na">clientId</span><span class="p">:</span> <span class="dl">'</span><span class="s1">..</span><span class="dl">'</span><span class="p">,</span>
<span class="na">clientSecret</span><span class="p">:</span> <span class="dl">'</span><span class="s1">..</span><span class="dl">'</span><span class="p">,</span>
<span class="na">server</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://my-auth-server.example</span><span class="dl">'</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">tokens</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">clientCredentials</span><span class="p">();</span>
</code></pre></div></div>
<p>Without the meta-data document, you will need to specify settings such as the
<code class="language-plaintext highlighter-rouge">tokenEndpoint</code> and possibly the <code class="language-plaintext highlighter-rouge">authorizationEndpoint</code> depending on which
flow you are using.</p>
<h2 id="authorization_code-example">authorization_code example</h2>
<p>The <code class="language-plaintext highlighter-rouge">authorization_code</code> flow is a multi-step process, so a bit more involved.
The library gives you direct access to the primitives, allowing you to
integrate in your own frameworks and applications.</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">OAuth2Client</span><span class="p">,</span> <span class="nx">generateCodeVerifier</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">@badgateway/oauth2-client</span><span class="dl">'</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">OAuth2Client</span><span class="p">({</span>
<span class="na">server</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://authserver.example/</span><span class="dl">'</span><span class="p">,</span>
<span class="na">clientId</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="c1">// Part of PCKE</span>
<span class="kd">const</span> <span class="nx">codeVerifier</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">generateCodeVerifier</span><span class="p">();</span>
<span class="c1">// In a browser this might work as follows:</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">location</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">authorizationCode</span><span class="p">.</span><span class="nx">getAuthorizeUri</span><span class="p">({</span>
<span class="na">redirectUri</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://my-app.example/</span><span class="dl">'</span><span class="p">,</span>
<span class="na">state</span><span class="p">:</span> <span class="dl">'</span><span class="s1">some-string</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">codeVerifier</span><span class="p">,</span>
<span class="na">scope</span><span class="p">:</span> <span class="p">[</span><span class="dl">'</span><span class="s1">scope1</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">scope2</span><span class="dl">'</span><span class="p">],</span>
<span class="p">});</span>
</code></pre></div></div>
<h3 id="handling-the-redirect-back">Handling the redirect back</h3>
<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">oauth2Token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nx">authorizationCode</span><span class="p">.</span><span class="nx">getTokenFromCodeRedirect</span><span class="p">(</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">location</span><span class="p">,</span>
<span class="p">{</span>
<span class="na">redirectUri</span><span class="p">:</span> <span class="dl">'</span><span class="s1">https://my-app.example/</span><span class="dl">'</span><span class="p">,</span>
<span class="na">state</span><span class="p">:</span> <span class="dl">'</span><span class="s1">some-string</span><span class="dl">'</span><span class="p">,</span>
<span class="nx">codeVerifier</span><span class="p">,</span>
<span class="p">}</span>
<span class="p">);</span>
</code></pre></div></div>
<h2 id="docs-and-download">Docs and download</h2>
<ul>
<li><a href="https://github.com/badgateway/oauth2-client" title="OAuth2 Client on github">Github</a></li>
<li><a href="https://www.npmjs.com/package/@badgateway/oauth2-client" title="OAuth2 Client on npmjs.org">npmjs</a></li>
</ul>
http://evertpot.com/no-dst-is-bad/Reasons why abolishing DST in the US will be worse for users and developers2022-03-18T08:51:00+00:00Evert Pot[email protected]<p>Daylight savings time is hated by many, and twice per year a discussion
reignites to get rid of it. Lot of folks feel this is a great idea. This year
<a href="https://www.reuters.com/world/us/us-senate-approves-bill-that-would-make-daylight-savings-time-permanent-2023-2022-03-15/?utm_source=reddit.com">this decision</a> seems especially close in the US. If this law passes, it
will probably also change where I live 🇨🇦.</p>
<p>No doubt there’s lots of benefits and advantages to this, I don’t want to
dispute that. This is also not an endorsement against that change, but I do
however want to bring light to at least one disadvantage:</p>
<p>The timezone change was for a lot of developers a twice-per-year reminder
that we need to use timezone databases and libraries.</p>
<p>This is useful, because every year many countries change their timezone
rules. This means that if you schedule something in the future, say.. 2pm 6
months from, you don’t know yet with absolute certainty what UTC timestamp
this will be. This is especially important when scheduling between people
in multiple timezones.</p>
<p>The right way to handle this is to store the intended local time + a location
such as <code class="language-plaintext highlighter-rouge">America/Toronto</code>. <code class="language-plaintext highlighter-rouge">EST</code> is not enough, because it’s <code class="language-plaintext highlighter-rouge">EDT</code> half the
year, and obviously neither is <code class="language-plaintext highlighter-rouge">-0500</code>. I grew up in the netherlands, and it
was only when I got into programming I realized that our timezone is not
<code class="language-plaintext highlighter-rouge">+0100</code> all year round, unlike what we learned in school.</p>
<p>Timezones are relatively stable in North America, but the US also made a
change <a href="https://www.cnn.com/2007/EDUCATION/03/07/extra.daylight.saving/index.html">in 2007</a>, and it sounds like we may have another one in the future!</p>
<p>So this bi-annual time change was a great reminder to many developers that
timezones are a thing, and you can’t just naively assume a UTC time + an
offset is enough. Even more so for teams that are spread cross-continent
because the DST change doesn’t fall on the same day. Currently I’m in the
3 weeks per year the time difference between me and my parents is 5
instead of 6 hours.</p>
<p>A lot of programming is (seems?) anglo-centric. A similar situation is that
before Emoji became wide-spread it was way more common to see a lot more
issues around encoding non-ascii characters 🤷 (<a href="https://news.ycombinator.com/item?id=30696850">billpg</a>). Especially in
languages that don’t have good native unicode support (looking at you PHP).</p>
<p>So if DST goes away in North America, I predict we’ll see more people assuming
using the offset is enough, resulting in bugs related to:</p>
<ul>
<li>Times in countries that have not yet abolished DST.</li>
<li>Countries that ever change timezone rules. (This happens more often than
you think!)</li>
<li>Applications that deal with historical data.</li>
</ul>
<p>It doesn’t help that one of the most common date formats (ISO 8601) uses an
offset! (<code class="language-plaintext highlighter-rouge">2022-03-18T17:05:30.996-0400</code>). This is OKish for things that have
already happened, but not good for anything in the future.</p>
<p>So when you hear developers excited about the US abolishing DST because it
will make their (work) life simpler, remind them this is only true if you
never intend your software to be used outside of North America, or when the
entire rest of the world makes the same change and also freezes all
timezone rules forever!</p>