<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bryan MacLee</title>
    <description>The latest articles on DEV Community by Bryan MacLee (@bryan_maclee).</description>
    <link>https://dev.to/bryan_maclee</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3886582%2F3438885a-1dec-48e3-8207-e888be43bb88.png</url>
      <title>DEV Community: Bryan MacLee</title>
      <link>https://dev.to/bryan_maclee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bryan_maclee"/>
    <language>en</language>
    <item>
      <title>The server boundary disappears</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Tue, 28 Apr 2026 16:50:19 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/the-server-boundary-disappears-hap</link>
      <guid>https://dev.to/bryan_maclee/the-server-boundary-disappears-hap</guid>
      <description>&lt;p&gt;&lt;em&gt;authored by claude, rubber stamped by Bryan MacLee&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: Stop writing API routes. The compiler does it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every framework dev has shipped a bug where they thought a function ran on the server but it didn't, or thought it ran on the client but it didn't. I am not an experienced framework developer. I can hobble through React if I HAVE TO. But across about twenty compiler attempts the same shape kept showing up: the compiler should know which side of the wire each function is on, because the wire is part of the program.&lt;/p&gt;

&lt;p&gt;I'm obsessed with performance. I'm also a believer in "do it right, the first time, even if it takes more time." The server boundary is a place where most languages do neither, and the runtime pays for both. So this is the second of six features the browser-language overview piece promised to unpack later. The boundary, in detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "shipping a typed POST endpoint" actually costs
&lt;/h2&gt;

&lt;p&gt;Pick a framework. Next, Remix, Express plus a React frontend, doesn't matter. The minimum table-stakes for a single server endpoint that takes some data, validates it, persists it, and returns a typed response looks like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/orders/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/lib/db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bad JSON&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// ... actual business logic finally starts here ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;On the client:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/cart/page.tsx&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submitOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitOrderInput&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SubmitOrderOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-csrf-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getToken&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SubmitOrderOutput&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the &lt;em&gt;minimum&lt;/em&gt;. No retry. No timeout. No proper error type. No loading state. The shapes are typed twice, once with zod and once with hand-written TS. The CSRF token plumbing is manual. If you change the input shape on the server and forget to update the client TS type, nothing fails until production traffic hits it.&lt;/p&gt;

&lt;p&gt;This is the seam. It is the most expensive seam in the application. It is also the seam that no framework can close, because the framework doesn't own both sides of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same feature in scrml
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt; db src="./app.db" tables="orders,order_items"&amp;gt;

server fn submitOrder(items: List&amp;lt;Item&amp;gt;(@length &amp;gt; 0 &amp;amp;&amp;amp; @length &amp;lt; 100)) -&amp;gt; OrderResult {
    let total = items.sum(it =&amp;gt; it.price * it.qty)
    let orderId = ?{`INSERT INTO orders (total) VALUES (${total}) RETURNING id`}.get().id
    items.forEach(it =&amp;gt; {
        ?{`INSERT INTO order_items (order_id, product_id, qty) VALUES (${orderId}, ${it.productId}, ${it.qty})`}.run()
    })
    return OrderResult { orderId: orderId, total: total }
}
&amp;lt;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the call site, anywhere in the same file or another &lt;code&gt;.scrml&lt;/code&gt; file in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let result = submitOrder(@cart.items)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire feature. Both halves.&lt;/p&gt;

&lt;p&gt;What the compiler did with &lt;code&gt;server fn&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generated a server-side route handler. Route name is compiler-internal; you don't reference it.&lt;/li&gt;
&lt;li&gt;Generated the client-side &lt;code&gt;fetch&lt;/code&gt; call that invokes the route, with arg serialization, response deserialization, and &lt;code&gt;await&lt;/code&gt; insertion. The developer SHALL NOT write &lt;code&gt;JSON.stringify&lt;/code&gt;, &lt;code&gt;JSON.parse&lt;/code&gt;, or &lt;code&gt;fetch&lt;/code&gt; to consume server function return values (§12.5).&lt;/li&gt;
&lt;li&gt;Type-checked the call site. &lt;code&gt;submitOrder(@cart.items)&lt;/code&gt; checks the argument shape against the server fn signature in the same compile pass. There is no client-side &lt;code&gt;type SubmitOrderInput&lt;/code&gt; declaration to drift.&lt;/li&gt;
&lt;li&gt;Emitted the function body to the server output only. The client gets a fetch stub.&lt;/li&gt;
&lt;li&gt;Enforced the predicate at the server boundary. &lt;code&gt;(@length &amp;gt; 0 &amp;amp;&amp;amp; @length &amp;lt; 100)&lt;/code&gt; is checked at function entry, on the server, before any database write. The check runs even if the client already validated (§53.9.4).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What the compiler refuses
&lt;/h2&gt;

&lt;p&gt;Six refusals, every one of them a real diagnostic with an E-code you can look up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Reading a server-only field on the client. (E-PROTECT-001)&lt;/strong&gt;&lt;br&gt;
If &lt;code&gt;&amp;lt; db&amp;gt;&lt;/code&gt; declares &lt;code&gt;protect="passwordHash"&lt;/code&gt;, then &lt;code&gt;passwordHash&lt;/code&gt; does not exist on the client type. A client-side function trying to read it is a compile error, not a runtime exposure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Code that accesses a protected field but might run client-side. (E-PROTECT-002)&lt;/strong&gt;&lt;br&gt;
The compiler verifies at compile time that no function accessing protected fields executes on the client. Any code path that could route to the client and touches a protected field fails compile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A server fn calling a client-only fn. (E-ROUTE-002)&lt;/strong&gt;&lt;br&gt;
If a &lt;code&gt;server fn&lt;/code&gt; transitively calls a function that touches the DOM or reads a client-only derived value, compile fails with the call chain printed. The error message names the server function, the client-only callee, and the path between them, then suggests three resolutions: extract a pure function, duplicate the logic, or re-evaluate the classification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. A non-serializable return type from a server fn. (E-ROUTE-003)&lt;/strong&gt;&lt;br&gt;
Try to return a function, a DOM node, or a class instance from &lt;code&gt;server fn&lt;/code&gt; and the compile fails. The wire is JSON; the type system enforces it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. A client-local &lt;code&gt;@var&lt;/code&gt; used as a bound parameter in INSERT, UPDATE, or DELETE outside a server fn. (E-AUTH-001)&lt;/strong&gt;&lt;br&gt;
The compiler refuses to silently persist client-local state. The error message tells the developer to pass the value to a server function first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. A predicate violation at the boundary. (E-CONTRACT-001 at compile time, E-CONTRACT-001-RT at the boundary.)&lt;/strong&gt;&lt;br&gt;
If the compiler can prove a literal violates a predicate, the build fails. If the value is only known at runtime, a server-side boundary check fires before any business logic runs and rejects the request.&lt;/p&gt;

&lt;p&gt;That's six refusals. Every one of them is a type-system answer to a question that, in framework land, is "hope your tests catch it."&lt;/p&gt;

&lt;h2&gt;
  
  
  What gets generated for free
&lt;/h2&gt;

&lt;p&gt;Beyond refusing the wrong things, the compiler also generates the things you would have written by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fetch stub.&lt;/strong&gt; Argument serialization, response deserialization, automatic &lt;code&gt;await&lt;/code&gt;. No &lt;code&gt;JSON.stringify&lt;/code&gt;. No &lt;code&gt;JSON.parse&lt;/code&gt;. No manual &lt;code&gt;await&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The route handler.&lt;/strong&gt; With its name as a compiler-internal detail you never see.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSRF plumbing, when &lt;code&gt;&amp;lt;program csrf="on"&amp;gt;&lt;/code&gt; is set.&lt;/strong&gt; A token-mint server fn, a &lt;code&gt;&amp;lt;meta name="csrf-token"&amp;gt;&lt;/code&gt; injection in the generated HTML, a request interceptor that adds the &lt;code&gt;X-CSRF-Token&lt;/code&gt; header to every state-mutating request, and a server-side validator that returns 403 if the token is missing or invalid (§39.2.3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predicate validation at the boundary.&lt;/strong&gt; Inline predicate constraints on server function parameters are enforced server-side, before any database write or business logic, independently of any client-side check. A server function's parameter constraint cannot be bypassed by raw HTTP requests (§53.9.4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async parallelization.&lt;/strong&gt; Independent server calls in the same function body are parallelized in generated code; dependent ones are sequenced. The developer writes flat synchronous-looking code; the compiler emits &lt;code&gt;Promise.all&lt;/code&gt; and &lt;code&gt;await&lt;/code&gt; correctly (§13.2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The compiler is the dev's best friend. That phrase comes up a lot in my notes. This is what it means in practice. Every line of the framework boilerplate above is moved into the compiler, where it cannot drift, cannot be skipped under deadline pressure, and cannot be wrong without the build failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this kills
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;app/api/&lt;/code&gt; directory. There aren't any route files. There are functions.&lt;/li&gt;
&lt;li&gt;Hand-written fetch wrappers. There is no &lt;code&gt;apiClient.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;tRPC and OpenAPI codegen steps. The type goes through the boundary natively because the compiler owns both sides.&lt;/li&gt;
&lt;li&gt;Zod-on-the-wire. Inline predicates &lt;em&gt;are&lt;/em&gt; the type. A schema file isn't a separate artifact. Why would anyone bring zod into a scrml project?&lt;/li&gt;
&lt;li&gt;Type-drift bugs that ship to prod. The client and server agree on the shape because there is one declaration.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is still real
&lt;/h2&gt;

&lt;p&gt;Server-side concerns don't disappear. They just stop being plumbing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auth.&lt;/strong&gt; Still your job. The &lt;code&gt;server&lt;/code&gt; annotation is described in the spec as a security escape hatch precisely because compile-time inference of "what touches protected data" is not always sufficient on its own (§11.4). Annotate auth-touching functions with &lt;code&gt;server&lt;/code&gt; explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting.&lt;/strong&gt; A &lt;code&gt;&amp;lt;program ratelimit="100/min"&amp;gt;&lt;/code&gt; attribute generates a sliding-window limiter (§39.2.4). Tune the rate to your business; the mechanism is built in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Input validation against business rules.&lt;/strong&gt; Predicates handle shape and range. Business rules ("this user can submit at most 3 of these per day") are still business logic. They live inside the &lt;code&gt;server fn&lt;/code&gt;. They benefit from running where the data lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The line between "plumbing the framework forced you to write" and "actual business logic" gets a lot brighter when one side of it is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper claim
&lt;/h2&gt;

&lt;p&gt;A reactive system that wires its dependencies at compile time does no work at runtime to figure out what to update. A query that batches itself at compile time doesn't need a DataLoader. A boundary that is enforced at compile time doesn't need a validator on the wire.&lt;/p&gt;

&lt;p&gt;The runtime does less because the compiler did more. The seam between client and server stops being a place where bugs live and starts being a place where the type system has the most leverage. That is the design. A little short of perfect is still pretty awesome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/why-programming-for-the-browser-needs-a-different-kind-of-language-6m2"&gt;Why programming for the browser needs a different kind of language&lt;/a&gt;. The high-altitude six-feature overview that this piece zooms in on.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/what-npm-package-do-you-actually-need-in-scrml-2247"&gt;What npm package do you actually need in scrml?&lt;/a&gt;. The package-list-collapses argument worked through one tier at a time.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/what-scrmls-lsp-can-do-that-no-other-lsp-can-and-why-giti-follows-from-the-same-principle-4899"&gt;What scrml's LSP can do that no other LSP can, and why giti follows from the same principle&lt;/a&gt;. What vertical integration unlocks for tooling and version control.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;Introducing scrml: a single-file, full-stack reactive web language&lt;/a&gt;. The starting-point overview if you haven't seen scrml before.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61"&gt;Null was a billion-dollar mistake. Falsy was the second.&lt;/a&gt;. On &lt;code&gt;not&lt;/code&gt;, presence as a type-system question, and why scrml refuses to inherit JavaScript's truthiness rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/scrmls-living-compiler-23f9"&gt;scrml's Living Compiler&lt;/a&gt;. The transformation-registry framing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scrml on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;github.com/bryanmaclee/scrmlTS&lt;/a&gt;. The working compiler, examples, spec, benchmarks.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>compilation</category>
      <category>programming</category>
    </item>
    <item>
      <title>What npm package do you actually need in scrml?</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Tue, 28 Apr 2026 16:48:02 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/what-npm-package-do-you-actually-need-in-scrml-2247</link>
      <guid>https://dev.to/bryan_maclee/what-npm-package-do-you-actually-need-in-scrml-2247</guid>
      <description>&lt;p&gt;&lt;em&gt;authored by claude, rubber stamped by Bryan MacLee&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: The npm package list you'd actually need in scrml is short. The critique is mostly cargo-culted muscle memory.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am a truck driver by trade. I program because I love solving puzzles. So I have always been much more the "roll your own" type. I mean, why should I let someone else have all the fun.&lt;/p&gt;

&lt;p&gt;Every time I ever had to actually involve Node into anything I was working on, I cringed, and would try desperately not to have to fall down that rabbit hole. Because, axiomatically, the only way out is through.&lt;/p&gt;

&lt;p&gt;But no matter how much I dreaded typing "node", what truly got my blood boiling was what I always have, and always will, consider the world's most cancerous leaky abstraction. NPM.&lt;/p&gt;

&lt;h2&gt;
  
  
  What npm package do you actually need in scrml?
&lt;/h2&gt;

&lt;p&gt;The obvious "weakness" someone could point at is that scrml doesn't have an easy npm-install path yet. That isn't a weakness. That's the &lt;em&gt;whole point&lt;/em&gt;. I built scrml in part because I was sick of pulling in 200 packages to do what one well-designed language should do natively.&lt;/p&gt;

&lt;p&gt;Now to be fair, there &lt;em&gt;is&lt;/em&gt; a real version of this critique, and I'm going to address it head-on. A &lt;code&gt;scrml vendor add &amp;lt;url&amp;gt;&lt;/code&gt; CLI is on the roadmap. Until it ships, ingestion of arbitrary client-side bundles is rougher than it should be. Fine. I'll concede that.&lt;/p&gt;

&lt;p&gt;But the &lt;em&gt;invalid&lt;/em&gt; version of the critique would be the implication that there's some long, essential list of npm packages a scrml app would want and can't have. So let's actually enumerate them. Because when you do, the list is comically short.&lt;/p&gt;

&lt;p&gt;Here's the punchline up front: &lt;strong&gt;the npm-interop critique is mostly cargo-culted muscle memory from the React/Node era.&lt;/strong&gt; Modern Bun + scrml's stdlib + scrml's language features collapse the whole package list down to about five categories, and only one of those (heavyweight client-side widgets like CodeMirror, three.js, and Leaflet) is a real story problem worth solving with vendor ingestion. The rest is a rounding error.&lt;/p&gt;

&lt;h2&gt;
  
  
  What npm typically supplies, and where each goes in a scrml app
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Replaced by the language itself
&lt;/h3&gt;

&lt;p&gt;These categories account for most of a typical Node project's &lt;code&gt;package.json&lt;/code&gt;. None of them have a place in a scrml app, because scrml &lt;em&gt;is&lt;/em&gt; this layer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework + state + routing + forms.&lt;/strong&gt; React, Vue, Svelte, Redux, Zustand, Pinia, react-router, vue-router, Formik, react-hook-form. scrml's reactive primitives (&lt;code&gt;@var&lt;/code&gt;, derived, effects), components, file-based routing, and bindings replace the entire stack. &amp;lt;!-- cite: bio §3d state-as-first-class voice-scrmlTS:290-291 + design-insights-2026-04-08 transformation-registry --&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ORM / query builder.&lt;/strong&gt; Prisma, Drizzle, Kysely, TypeORM, Sequelize. scrml's &lt;code&gt;?{}&lt;/code&gt; SQL block writes parameterized queries directly against Bun.SQL, with compile-time schema introspection (§39) and protected-field enforcement (§11), and &lt;code&gt;?{}&lt;/code&gt; itself is specified at §44. &amp;lt;!-- cite: SPEC.md §44 ?{} multi-database adaptation; §39 schema and migrations; §11 protect= --&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS-in-JS / scoping.&lt;/strong&gt; styled-components, emotion, vanilla-extract. scrml's &lt;code&gt;#{}&lt;/code&gt; scoped CSS uses native &lt;code&gt;@scope&lt;/code&gt; (§9.1, §25.6). &amp;lt;!-- cite: SPEC.md §9.1 inline CSS line 4918, §25.6 native &lt;a class="mentioned-user" href="https://dev.to/scope"&gt;@scope&lt;/a&gt; line 11579 --&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP client.&lt;/strong&gt; axios, got, ky. Server fns can call &lt;code&gt;fetch&lt;/code&gt; directly. Markup-side fetches use &lt;code&gt;&amp;lt;request&amp;gt;&lt;/code&gt; and &lt;code&gt;lift&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build / bundle / dev server.&lt;/strong&gt; Vite, Webpack, esbuild, Parcel. &lt;code&gt;scrml dev&lt;/code&gt;, &lt;code&gt;scrml build&lt;/code&gt;, &lt;code&gt;scrml serve&lt;/code&gt;. Done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test runner.&lt;/strong&gt; vitest, jest, mocha. &lt;code&gt;bun test&lt;/code&gt;, plus the &lt;code&gt;scrml:test&lt;/code&gt; stdlib for assertions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket plumbing.&lt;/strong&gt; socket.io, ws. scrml's &lt;code&gt;&amp;lt;channel&amp;gt;&lt;/code&gt; (§38). &amp;lt;!-- cite: SPEC.md §38 WebSocket Channels --&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth / CSRF middleware.&lt;/strong&gt; passport, csurf, express-session. Boundary security is enforced by the compiler. CSRF mint-on-403 is built in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validation (the zod case).&lt;/strong&gt; zod, yup, joi, ajv, superstruct. I have two answers here, and both of them are stronger than zod:

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;§53 inline type predicates&lt;/strong&gt; are compile-time-enforced refinement types: &lt;code&gt;let x: number(&amp;gt;0 &amp;amp;&amp;amp; &amp;lt;10000)&lt;/code&gt;, &lt;code&gt;fn process(amount: number(&amp;gt;0 &amp;amp;&amp;amp; &amp;lt;10000))&lt;/code&gt;, &lt;code&gt;type Invoice:struct = { amount: number(&amp;gt;0 &amp;amp;&amp;amp; &amp;lt;10000) }&lt;/code&gt;. Named shapes like &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt; are first-class (and so are &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;color&lt;/code&gt;). Violations are compile errors (E-CONTRACT-001), not runtime exceptions you find out about when production blows up. &lt;strong&gt;Zod can't fail your build. This can.&lt;/strong&gt; &amp;lt;!-- cite: SPEC.md §53.6.1 named shape registry; type-system.ts:538 NAMED_SHAPES live registry --&amp;gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scrml:data/validate&lt;/code&gt;&lt;/strong&gt; stdlib for runtime form validation when the data shape is genuinely unknown until runtime: &lt;code&gt;validate(data, schema)&lt;/code&gt; returns &lt;code&gt;{ field: errors[] }&lt;/code&gt;, with rule builders for &lt;code&gt;required&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;minLength&lt;/code&gt;, &lt;code&gt;maxLength&lt;/code&gt;, &lt;code&gt;pattern&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;/&lt;code&gt;max&lt;/code&gt;, &lt;code&gt;numeric&lt;/code&gt;, &lt;code&gt;integer&lt;/code&gt;, &lt;code&gt;matches&lt;/code&gt;, &lt;code&gt;oneOf&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, plus domain composites (&lt;code&gt;emailField&lt;/code&gt;, &lt;code&gt;passwordField&lt;/code&gt;, &lt;code&gt;passwordConfirmField&lt;/code&gt;). &amp;lt;!-- cite: stdlib/data/validate.scrml lines 70-245 --&amp;gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever installed all of the above into a single React app, you've installed roughly 60-80% of the average &lt;code&gt;package.json&lt;/code&gt; line count. None of it belongs in a scrml app. Ever.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replaced by Bun
&lt;/h3&gt;

&lt;p&gt;Bun is the runtime, and Bun's stdlib has steadily absorbed the rest of the lower-level Node ecosystem. Every one of these used to be an npm package. Now they're built in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bcrypt&lt;/code&gt; becomes &lt;code&gt;Bun.password&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jsonwebtoken&lt;/code&gt; / &lt;code&gt;jose&lt;/code&gt; become web crypto + &lt;code&gt;Bun.password&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg&lt;/code&gt;, &lt;code&gt;mysql2&lt;/code&gt;, &lt;code&gt;better-sqlite3&lt;/code&gt; become &lt;code&gt;Bun.SQL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ioredis&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt; become &lt;code&gt;Bun.redis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sharp&lt;/code&gt; (some cases) becomes &lt;code&gt;Bun.spawn&lt;/code&gt; to imagemagick, or vendor when you really need to&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nodemailer&lt;/code&gt; becomes &lt;code&gt;Bun.spawn&lt;/code&gt; to system MTA, or REST-call a transactional email API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dotenv&lt;/code&gt; is built into Bun&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fs-extra&lt;/code&gt; is just Bun's &lt;code&gt;fs&lt;/code&gt; ergonomics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is what I mean when I say "roll your own": Bun's authors did. Now nobody has to npm-install bcrypt and pray the maintainer doesn't get bored.&lt;/p&gt;

&lt;h3&gt;
  
  
  Already in scrml's stdlib
&lt;/h3&gt;

&lt;p&gt;The 13-module stdlib already covers most of the "I'd npm install a small utility" reflex. I built it intentionally small but not so small that you have to leave the language for the basics:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stdlib module&lt;/th&gt;
&lt;th&gt;replaces&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;data/validate&lt;/code&gt; (+ §53)&lt;/td&gt;
&lt;td&gt;zod, yup, joi&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;data/transform&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;lodash (&lt;code&gt;pick&lt;/code&gt;, &lt;code&gt;omit&lt;/code&gt;, &lt;code&gt;groupBy&lt;/code&gt;, &lt;code&gt;sortBy&lt;/code&gt;, &lt;code&gt;unique&lt;/code&gt;, &lt;code&gt;flatten&lt;/code&gt;, ...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;auth&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;bcrypt, jsonwebtoken, speakeasy (TOTP), express-rate-limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;crypto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;crypto-js, bcryptjs, hashing helpers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;axios, got, node-fetch (typed wrapper with timeout + retry)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;time&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;date-fns / dayjs (basic format), lodash.debounce/throttle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;slugify, change-case, pluralize, currency/number formatting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;store&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;KV store, session store, counter (replaces &lt;code&gt;connect-sqlite3&lt;/code&gt; and basic redis use)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;router&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;path-to-regexp, qs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;test&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;chai, parts of jest/expect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;path&lt;/code&gt;, &lt;code&gt;process&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Node compat layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The stdlib isn't trying to be everything. It covers the high-frequency reaches. Specialty libraries get vendored. That's the deal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trivially vendored or rewritten
&lt;/h3&gt;

&lt;p&gt;Things that are honestly small enough to copy straight into your project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;uuid&lt;/code&gt;, &lt;code&gt;nanoid&lt;/code&gt;. One-liners against &lt;code&gt;crypto.randomUUID()&lt;/code&gt; or web crypto. Why are these npm packages?&lt;/li&gt;
&lt;li&gt;More date math beyond the stdlib. Vendor a single function from &lt;code&gt;date-fns&lt;/code&gt;. Don't drag in the whole library.&lt;/li&gt;
&lt;li&gt;Markdown rendering for short content. &lt;code&gt;marked&lt;/code&gt; is small and CDN-vendorable.&lt;/li&gt;
&lt;li&gt;Most of the &lt;code&gt;@types/*&lt;/code&gt; ecosystem. Irrelevant. scrml has its own type system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your "I need this from npm" instinct fires for one of these, the cost-benefit of vendoring a 40-line helper vs. wiring up a package manager flow isn't even close. Just write the function. Have the fun.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service SDKs are mostly thin REST wrappers
&lt;/h3&gt;

&lt;p&gt;This is the category most often invoked as a counterexample, and it's the one that drives me up a wall, because it's mostly a misconception:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stripe, OpenAI, Anthropic, Resend, SendGrid, Twilio, Slack, GitHub.&lt;/strong&gt; These are REST APIs. Their official SDKs are typed wrappers around &lt;code&gt;fetch&lt;/code&gt;. Calling the REST endpoint directly from a server fn is a 10-line &lt;code&gt;fetch&lt;/code&gt; call. Yes, the SDK convenience is real (typed responses, retry logic, pagination helpers). No, it isn't load-bearing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS SDK.&lt;/strong&gt; Somewhat heavier (SigV4 request signing), but you can either vendor the v3 modular packages or write a small SigV4 helper. People sign requests in 30 lines of bash. You can do it in scrml.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A scrml convention of "here's the canonical pattern for hitting Stripe / OpenAI / AWS from a server fn" docs page closes most of this gap. The capability already exists. The recipe doesn't, yet. That's a docs problem, not a language problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where npm interop actually bites
&lt;/h3&gt;

&lt;p&gt;Here's the honest list. The places where I'll grant you the criticism has teeth. These are heavyweight client-side libraries that you cannot reasonably re-implement, no matter how much I love rolling my own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code editors.&lt;/strong&gt; CodeMirror 6, Monaco, ProseMirror, TipTap, Lexical. 100k+ LOC each. (6nz already vendors CM6 via dynamic import + a &lt;code&gt;__cmMod&lt;/code&gt; global bridge. It's a working pattern.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D.&lt;/strong&gt; three.js, babylon.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maps.&lt;/strong&gt; Leaflet, mapbox-gl, MapLibre&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Charts beyond stdlib.&lt;/strong&gt; Chart.js, ECharts, D3, Plotly, Highcharts (scrml has &lt;code&gt;chart-utils.js&lt;/code&gt; for the lighter end)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PDF generation.&lt;/strong&gt; pdf-lib, jspdf&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Animation beyond CSS.&lt;/strong&gt; Framer Motion, GSAP, anime.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time collab CRDTs.&lt;/strong&gt; Yjs, Automerge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rich graph viz.&lt;/strong&gt; dagre, vis-network, cytoscape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is the same pattern: a heavyweight bundle that needs to load on the client, get a JS handle, and be called from app code. &lt;strong&gt;One mechanism solves the whole class:&lt;/strong&gt; &lt;code&gt;scrml vendor add &amp;lt;url&amp;gt;&lt;/code&gt; ingests a UMD or ES bundle from a CDN, generates a type shim, and wires it into the boundary security model. The 6nz CM6 integration is a working proof-of-concept already.&lt;/p&gt;

&lt;p&gt;That's the entire honest list. About ten categories of widget. Not "an open-ended ecosystem of two million packages."&lt;/p&gt;

&lt;h2&gt;
  
  
  So what's the strategic gap?
&lt;/h2&gt;

&lt;p&gt;The critique stops landing once I ship three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;scrml vendor add &amp;lt;url&amp;gt;&lt;/code&gt; CLI.&lt;/strong&gt; Flat-file ingestion of a CDN bundle, type-shim generation, manifest tracking. On the roadmap and on me to land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A type-shim story for vendored bundles.&lt;/strong&gt; The moment an adopter does &lt;code&gt;vendor add chart.js&lt;/code&gt;, they hit "untyped global." A canonical pattern (declare-only &lt;code&gt;.d.scrml&lt;/code&gt; or equivalent) closes this. I'm building it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "calling external REST SDKs" recipes doc.&lt;/strong&gt; Five examples (Stripe, OpenAI, AWS S3, Resend, Slack webhook) showing the &lt;code&gt;fetch&lt;/code&gt;-from-server-fn pattern with auth headers and typed responses. Docs work. I'll write it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once those three are in, the npm critique loses about 90% of its bite. The remaining 10% is the heavy widget category, and the answer there is "vendor the bundle. We will never npm-install three.js, and that's fine."&lt;/p&gt;

&lt;h2&gt;
  
  
  The deeper point
&lt;/h2&gt;

&lt;p&gt;The npm ecosystem is enormous &lt;em&gt;because&lt;/em&gt; the JavaScript language and the browser platform are minimal. Most of those packages exist to paper over missing primitives. A state library to give you reactivity, a router to give you routing, a CSS-in-JS library to give you scoped styles, an ORM to give you queries, a validation library to give you types at the boundary. When the language and runtime supply those primitives natively, the package list collapses.&lt;/p&gt;

&lt;p&gt;That's the bet I'm making with scrml. A first-principles, full-stack language with a real type system, a real reactive model, real boundary security, real query syntax, and a small focused stdlib is a smaller surface to learn and a smaller surface to maintain than a Node project that wires together 200 packages to recreate the same capabilities. The npm-interop critique reads as a weakness only if you assume the package list is a fixed cost. It isn't. &lt;strong&gt;It's the symptom.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The one package you actually need is &lt;code&gt;vendor add chart.js&lt;/code&gt;. I'm shipping it. Once it lands, the conversation is over.&lt;/p&gt;

&lt;p&gt;And me? I'll be back in the cab, thinking about the next puzzle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/why-programming-for-the-browser-needs-a-different-kind-of-language-6m2"&gt;Why programming for the browser needs a different kind of language&lt;/a&gt;. The companion piece. What a browser-shaped language actually owns at the type level.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/what-scrmls-lsp-can-do-that-no-other-lsp-can-and-why-giti-follows-from-the-same-principle-4899"&gt;What scrml's LSP can do that no other LSP can, and why giti follows from the same principle&lt;/a&gt;. What vertical integration unlocks, in two pieces of the ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;Introducing scrml: a single-file, full-stack reactive web language&lt;/a&gt;. The starting-point overview if you haven't seen scrml before.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61"&gt;Null was a billion-dollar mistake. Falsy was the second.&lt;/a&gt;. On &lt;code&gt;not&lt;/code&gt;, presence as a type-system question, and why scrml refuses to inherit JavaScript's truthiness rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/scrmls-living-compiler-23f9"&gt;scrml's Living Compiler&lt;/a&gt;. The transformation-registry framing. The constructive flip-side of the npm critique above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scrml on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;github.com/bryanmaclee/scrmlTS&lt;/a&gt;. The working compiler, examples, spec, benchmarks.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>compiler</category>
    </item>
    <item>
      <title>What scrml's LSP can do that no other LSP can, and why giti follows from the same principle</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Tue, 28 Apr 2026 16:45:52 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/what-scrmls-lsp-can-do-that-no-other-lsp-can-and-why-giti-follows-from-the-same-principle-4899</link>
      <guid>https://dev.to/bryan_maclee/what-scrmls-lsp-can-do-that-no-other-lsp-can-and-why-giti-follows-from-the-same-principle-4899</guid>
      <description>&lt;p&gt;&lt;em&gt;authored by claude, rubber stamped by Bryan MacLee&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR: mainstream LSPs are unions of partial language services. scrml's LSP is a single service that knows every context. giti is the same idea applied to version control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The LSP and giti both sit downstream of one integrated compiler that owns markup, logic, SQL, components, and reactivity in one AST. That single fact is what this piece is about, because it is what lets both of them ship features that piecewise alternatives in mainstream stacks literally cannot.&lt;/p&gt;

&lt;p&gt;That structural difference unlocks features the union model literally cannot ship. Giti is a parallel structural argument: a version control surface designed for the median developer, with the scrml compiler as its review gate. The same "single integrated thing knows everything" pattern is what makes it possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: The LSP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why mainstream LSPs hit a ceiling
&lt;/h3&gt;

&lt;p&gt;Take Vue. A &lt;code&gt;.vue&lt;/code&gt; file has three contexts: &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; (HTML), &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; (CSS), &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; (TypeScript). Volar (Vue's LSP) solves this with a thin LSP shell that delegates each region to a dedicated language service: HTML service, CSS service, TS service. To get TS-level features inside the template, Volar transforms the SFC to virtual TypeScript (&lt;code&gt;svelte2tsx&lt;/code&gt;-style) and asks tsserver. It works, and it's a real engineering achievement. But the union approach has a hard ceiling: each per-context service knows only its own context.&lt;/p&gt;

&lt;p&gt;The thing every Vue dev has hit: you can't get column completion in a SQL string inside a Vue script setup, because no Vue language service knows your SQL schema. The Volar architecture &lt;em&gt;can't&lt;/em&gt; know. Neither the TS service nor the CSS service nor the HTML service was designed to understand a SQL DSL embedded inside a string literal. You'd have to write a new "SQL inside JS string templates" plugin, plumb it into Volar's per-context dispatch, and then teach it about your specific schema source. Nobody does this.&lt;/p&gt;

&lt;p&gt;dbt has a SQL LSP. It does column completion against a schema. But dbt's LSP knows only SQL: your application code is invisible to it.&lt;/p&gt;

&lt;p&gt;That's the union-of-services ceiling. Each service is good at its one context, but the &lt;em&gt;interesting&lt;/em&gt; completions are at the boundaries between contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why scrml doesn't hit it
&lt;/h3&gt;

&lt;p&gt;scrml's compiler owns every context in one AST. A &lt;code&gt;.scrml&lt;/code&gt; file's markup, logic, components, SQL, CSS, and reactive declarations are all parsed by the same pipeline (PP, BS, TAB, MOD, CE, PA, RI, TS, META, DG, BP, CG). The PA (protect= Analyzer) pass already builds a &lt;code&gt;views&lt;/code&gt; map keyed by &lt;code&gt;&amp;lt; db&amp;gt;&lt;/code&gt; block, and each entry carries every table's full schema. That data is sitting in memory the moment the LSP runs analysis on a buffer.&lt;/p&gt;

&lt;p&gt;So when you type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt; db src="./app.db" tables="users,posts"&amp;gt;

server fn list_recent() {
    return ?{`
        SELECT u.|     -- cursor here
        FROM users u
    `}.all()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the LSP can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect the cursor is in a SQL context.&lt;/li&gt;
&lt;li&gt;Parse the partial SQL to find table alias &lt;code&gt;u&lt;/code&gt; resolves to &lt;code&gt;users&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Pull the column list for &lt;code&gt;users&lt;/code&gt; from the PA result (&lt;code&gt;protectAnalysis.views.get(stateBlockId).tables.get("users").fullSchema&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Return those columns as completions, each labeled with its SQL type, primary-key/index status, and protected-field status.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is &lt;strong&gt;a feature zero competitor LSPs offer&lt;/strong&gt;, because no competitor LSP owns both your application code AND your database schema in the same analysis pass. dbt's LSP knows your SQL but not your application. tsserver knows your application but not your SQL strings. Volar knows your Vue template but not your SQL strings. The union model can't get there from here.&lt;/p&gt;

&lt;p&gt;The same structural argument extends to:&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-file component prop completion
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// components/card.scrml
export const Card = &amp;lt;article props={ title: string, body: string, publishedAt: Date }&amp;gt;
    &amp;lt;h2&amp;gt;${title}&amp;lt;/&amp;gt;
    &amp;lt;p&amp;gt;${body}&amp;lt;/&amp;gt;
    &amp;lt;span class=date&amp;gt;${publishedAt}&amp;lt;/&amp;gt;
&amp;lt;/article&amp;gt;

// pages/index.scrml
import { Card } from "../components/card.scrml"

&amp;lt;Card title="Hi" |    -- cursor here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LSP suggests &lt;code&gt;body=&lt;/code&gt;, &lt;code&gt;publishedAt=&lt;/code&gt;. Cross-file. Driven by a derived prop registry built from &lt;code&gt;ComponentDefNode.raw&lt;/code&gt; parsed at workspace-bootstrap time. &lt;strong&gt;The L3 phase of the LSP roadmap landed this in S40.&lt;/strong&gt; It works against &lt;code&gt;export.raw&lt;/code&gt; synthesized component-defs, not just same-file components.&lt;/p&gt;

&lt;p&gt;A React or Vue dev reading this is thinking "my IDE has done this for years". Yes, for components written in JSX or SFCs that tsserver / Volar can analyze. &lt;strong&gt;scrml's version works for components defined in markup-first source that tsserver and Volar would never touch.&lt;/strong&gt; And it works because &lt;code&gt;&amp;lt;Card title="Hi"&lt;/code&gt; and &lt;code&gt;&amp;lt;article props={...}&amp;gt;&lt;/code&gt; are nodes in the same AST pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-file go-to-definition that's actually accurate
&lt;/h3&gt;

&lt;p&gt;This is the table-stakes feature TypeScript devs ship by default. scrml shipped it in L2 (S40) via a workspace cache that holds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;exportRegistry: Map&amp;lt;filePath, Map&amp;lt;exportName, ExportInfo&amp;gt;&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fileASTMap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;importGraph&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cache rebuilds on &lt;code&gt;didChange&lt;/code&gt; / &lt;code&gt;didOpen&lt;/code&gt;. If the touched file's export shape changed, ALL open buffers re-analyze. F12 on a cross-file &lt;code&gt;Card&lt;/code&gt; jumps to the &lt;code&gt;ComponentDefNode.span&lt;/code&gt; in &lt;code&gt;components/card.scrml&lt;/code&gt;. F12 on an imported function jumps to the &lt;code&gt;export-decl&lt;/code&gt; span. Same file or other file: same code path.&lt;/p&gt;

&lt;p&gt;This is unremarkable in mature LSPs. It is remarkable that scrml (a young language) has it working today, because the alternative was the route most young languages take: same-file go-to-def, "cross-file is a future feature", and the dev experience suffers for years. The structural reason scrml could ship it early is the same one that makes SQL completion possible: one compiler owns everything, so wiring MOD output (the export graph) into the LSP is one cache layer, not one cache layer per language service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code actions that quick-fix scrml-specific errors
&lt;/h3&gt;

&lt;p&gt;L4 (S40) shipped &lt;code&gt;codeActionProvider&lt;/code&gt; quick-fixes for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E-IMPORT-004&lt;/strong&gt;: Levenshtein-rank closest exported name from the imported module's actual exports. ("&lt;code&gt;Cardd&lt;/code&gt; not exported from &lt;code&gt;./card.scrml&lt;/code&gt;. Did you mean &lt;code&gt;Card&lt;/code&gt;?")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-IMPORT-005&lt;/strong&gt;: bare specifier missing &lt;code&gt;./&lt;/code&gt; prefix. (Auto-prefix.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-LIN-001&lt;/strong&gt;: unconsumed linear value. (Auto-prefix the binding with &lt;code&gt;_&lt;/code&gt; to silence.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-PA-007&lt;/strong&gt;: column not in table schema. &lt;strong&gt;Levenshtein-ranks the closest column from PA's &lt;code&gt;views&lt;/code&gt;.&lt;/strong&gt; ("&lt;code&gt;name&lt;/code&gt; not in &lt;code&gt;users&lt;/code&gt;. Did you mean &lt;code&gt;username&lt;/code&gt;?")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;E-SQL-006&lt;/strong&gt;: &lt;code&gt;.prepare()&lt;/code&gt; removed in §44 Bun.SQL migration. (Strip the call.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice E-PA-007. The quick-fix is "did you mean &lt;code&gt;username&lt;/code&gt;?" pulled from the schema introspection that PA already did. &lt;strong&gt;This requires the LSP to have your DB schema in the same analysis pass as your error diagnosis.&lt;/strong&gt; Other tooling stacks have to either ship a separate "schema linter" tool that doesn't know your application code, or skip the suggestion. scrml's LSP suggests it inline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signature help that crosses files
&lt;/h3&gt;

&lt;p&gt;L4 also shipped &lt;code&gt;signatureHelpProvider&lt;/code&gt; triggered on &lt;code&gt;(&lt;/code&gt; and &lt;code&gt;,&lt;/code&gt;. For cross-file imported functions, the LSP synthesizes the function shape from the export's &lt;code&gt;raw&lt;/code&gt; source (parsed at workspace-bootstrap). So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { computeTotal } from "./billing.scrml"

const total = computeTotal(|    -- cursor here, signature popup shows:
                                --   computeTotal(items: List&amp;lt;Item&amp;gt;, taxRate: Number)
                                --                ↑ active param
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;works without the called function's source being open in the editor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hover with reactive/tilde badges and state field types
&lt;/h3&gt;

&lt;p&gt;Same-file or cross-file, hover shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;function signature plus boundary (server vs. client)&lt;/li&gt;
&lt;li&gt;reactive variable badges (&lt;code&gt;@count&lt;/code&gt; is reactive; &lt;code&gt;~tmp&lt;/code&gt; is tilde-decl)&lt;/li&gt;
&lt;li&gt;struct field types from &lt;code&gt;&amp;lt;state&amp;gt;&lt;/code&gt; blocks&lt;/li&gt;
&lt;li&gt;enum variant payload shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't novel by itself. Every LSP does hover. The differentiator is what's &lt;em&gt;in&lt;/em&gt; the hover. The "boundary" badge is impossible in TypeScript LSP, because TS doesn't have a server/client boundary concept. That's a scrml compile-time invariant the compiler enforces (SPEC §11 protected fields, §12 route inference). Showing it in hover means a dev never has to wonder "is this function safe to call from client code?". The LSP tells them on mouse-over.&lt;/p&gt;

&lt;h3&gt;
  
  
  Document symbols with semantic meaning
&lt;/h3&gt;

&lt;p&gt;The outline panel (L1, S40) populates with: &lt;code&gt;&amp;lt;state&amp;gt;&lt;/code&gt; blocks, components, server/client functions, machines, &lt;code&gt;&amp;lt;db&amp;gt;&lt;/code&gt; blocks. Each gets a symbol kind (Variable / Class / Function / Module) appropriate to scrml's mental model, not JS's. A scrml dev sees their &lt;code&gt;state&lt;/code&gt; blocks as first-class entities in the outline. A TS dev with a similar Zustand store sees a generic function. The LSP can show the structure the dev actually thinks in.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's deliberately NOT shipped (and why)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic tokens (L5)&lt;/strong&gt;: formally dropped from the active roadmap per a 6nz consultation. 6nz is the editor in the scrml ecosystem and is moving toward spatial annotation panels, where coloring is a side channel, not the primary signal carrier. TextMate handles broad-strokes coloring fine. Semantic tokens would be sunk cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find-references&lt;/strong&gt;: pending. Will use the workspace cache (already built for L2/L3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rename-symbol&lt;/strong&gt;: pending. Same dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspace-symbol search&lt;/strong&gt;: pending. Cheap on top of the existing export registry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deferral pattern is informative. Every L1-L4 capability either uses a single-file AST walk or the workspace cache. Nothing scrml's LSP is shipping today is built on top of speculative architecture. Everything is downstream of pipeline stages that already exist for the compiler to do its main job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;The LSP capabilities you can ship are gated by what the compiler can tell you. mainstream LSPs ship a union of per-context services because mainstream languages are unions of per-context tools. scrml's compiler owns every context, so the LSP can ask one analysis pass for everything: markup, logic, SQL schemas, component prop registries, cross-file imports, error-fix suggestions. And surface it without per-context plumbing.&lt;/p&gt;

&lt;p&gt;That's why SQL column completion against your live schema is a 50-line LSP feature for scrml and an open research project for everyone else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2: Why giti follows from the same principle
&lt;/h2&gt;

&lt;p&gt;scrml's LSP works because one compiler owns every context. Giti works because &lt;strong&gt;one platform owns the entire collaboration surface&lt;/strong&gt;, and uses the scrml compiler as its review gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Git's mental-model problem
&lt;/h3&gt;

&lt;p&gt;The numbers (giti-spec-v1.md §1.2):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;52% of all developers struggle with git at least once a month&lt;/li&gt;
&lt;li&gt;75% of self-described "confident" git users still struggle monthly&lt;/li&gt;
&lt;li&gt;87% have hit merge conflicts they didn't know how to resolve&lt;/li&gt;
&lt;li&gt;65% have lost commits or changes&lt;/li&gt;
&lt;li&gt;55% find rebase error-prone&lt;/li&gt;
&lt;li&gt;45% have been negatively affected by a colleague's force push&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standard response to these numbers is "users need more git education". giti's design rejects this. The mental model itself is the defect. Anything that requires understanding git internals (staging vs. working tree, detached HEAD, fast-forward vs. merge commits, the interaction between local and remote refs, what &lt;code&gt;git reset --soft&lt;/code&gt; vs. &lt;code&gt;--mixed&lt;/code&gt; vs. &lt;code&gt;--hard&lt;/code&gt; actually does) is a defect in giti's surface, not a user education gap.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 5-function surface
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;giti save     : snapshot working state (no staging area; everything is included)
giti switch   : move to a different point in history
giti merge    : bring another line of work in
giti undo     : reverse the last operation
giti history  : show what happened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire daily-development vocabulary. There is no &lt;code&gt;add&lt;/code&gt;. No &lt;code&gt;stash&lt;/code&gt; (working copy IS a commit, courtesy of jj-lib's storage model). No &lt;code&gt;reset&lt;/code&gt; family with three flavors. No detached HEAD. No &lt;code&gt;rebase --interactive&lt;/code&gt;. No &lt;code&gt;cherry-pick&lt;/code&gt;. No &lt;code&gt;reflog&lt;/code&gt; to recover from a destructive operation, because no destructive operation exists at the surface.&lt;/p&gt;

&lt;p&gt;The 5-function design wasn't pulled from the air. It was derived from actual usage data of the project that built giti:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;767 saves&lt;/li&gt;
&lt;li&gt;705 context switches&lt;/li&gt;
&lt;li&gt;206 merges&lt;/li&gt;
&lt;li&gt;146 undos&lt;/li&gt;
&lt;li&gt;44 stashes (all orphaned. Nobody recovered work from them. The stash model is broken.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5 operations cover what people &lt;em&gt;do&lt;/em&gt;. The rest of git's surface is what people learn to &lt;em&gt;avoid&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The scrml compiler is the reviewer
&lt;/h3&gt;

&lt;p&gt;86% of pull request lead time is waiting for human review. For solo developers and small teams, that's pure friction with no information gain. A human reviewer reads a 30-line diff and approves it because the change is obvious. The bottleneck doesn't add quality. It adds latency.&lt;/p&gt;

&lt;p&gt;giti &lt;code&gt;land&lt;/code&gt; runs the scrml compiler over the changed files and the test suite. If both pass, the change lands. If either fails, the dev gets the same error their LSP would show: same diagnostic, same E-code, same span. &lt;strong&gt;There's no "the CI is broken but my local works" because the LSP and the gate share the compiler.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For teams that DO need human review, giti supports it as a layered primitive (Landing, Stack, TypedChange in §5 of the spec). Human review still happens. It just isn't the only gate.&lt;/p&gt;

&lt;p&gt;The structural argument is identical to the LSP argument: when one tool owns the whole pipeline, you can use it as the review surface. You can't do this with git plus GitHub plus CircleCI plus Codecov plus Sonar. Those are independent services that have to negotiate. You can do it with giti plus scrmlTS, because they share an in-process compiler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conflict-as-data instead of conflict-as-disaster
&lt;/h3&gt;

&lt;p&gt;jj-lib (giti's underlying engine) treats conflicts as &lt;em&gt;first-class data&lt;/em&gt; in the working copy, not as an error state that halts work. A merge with conflicts produces a working state that contains the conflict explicitly. You can keep editing. You can switch away and come back. You can run tests against the conflicted state. The "you can't do anything until you resolve the conflict" failure mode that everyone has hit in git doesn't exist at giti's surface.&lt;/p&gt;

&lt;p&gt;The longer-term play (spec §3.7, "engine independence milestone") is that scrml's compiler can do AST-level conflict resolution that text-merge tools can't. When two devs renamed the same function in different ways, the compiler knows which references resolve to which definition, and the merge proposes a coherent answer instead of "here are conflict markers, you sort it out".&lt;/p&gt;

&lt;h3&gt;
  
  
  Private scopes instead of &lt;code&gt;.gitignore&lt;/code&gt; plus private repos
&lt;/h3&gt;

&lt;p&gt;scrml apps frequently have files that are local-dev-only: secrets, machine-specific config, private notes. The git answer is "keep two repos" or "abuse &lt;code&gt;.gitignore&lt;/code&gt; and pray". giti has private scopes (§12): a &lt;code&gt;.giti/private&lt;/code&gt; manifest, glob-based, with engine-level routing that keeps private commits on a &lt;code&gt;_private&lt;/code&gt; bookmark and refuses to push private content to public remotes.&lt;/p&gt;

&lt;p&gt;Slices 1-5 have shipped (private add/remove/list, remote scope config, save-time scope classification, push safety, automatic split for mixed working copies). The mechanism is part of the platform, not a workaround over it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crash recovery built in
&lt;/h3&gt;

&lt;p&gt;Because jj's working-copy-is-a-commit model continuously tracks the working directory, unsaved edits are recoverable after a crash. A process kill between saves doesn't destroy work. &lt;code&gt;giti undo&lt;/code&gt; or &lt;code&gt;giti history --ops&lt;/code&gt; shows the last working state. There is no equivalent to "I forgot to commit and lost three hours of work".&lt;/p&gt;

&lt;h3&gt;
  
  
  The shared structural argument
&lt;/h3&gt;

&lt;p&gt;Both LSP and giti make the same bet: when one piece of software owns the whole surface, you can ship features that piecewise alternatives cannot. The LSP knows your DB schema and your application code in the same analysis pass, so it can suggest "did you mean &lt;code&gt;username&lt;/code&gt;?" for an unknown SQL column. giti knows your compiler and your test suite as in-process libraries, so it can use them as the review gate without a CI round-trip.&lt;/p&gt;

&lt;p&gt;The Volar/dbt/tsserver world cannot get to scrml's LSP capabilities by adding more services. The integration cost grows quadratically with the number of contexts. The git/GitHub/CI world cannot get to giti's land workflow by adding more bots. The negotiation cost grows quadratically with the number of integrations.&lt;/p&gt;

&lt;p&gt;Vertical integration of a thoughtful design is the only path through this. scrml plus giti is what that path looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/why-programming-for-the-browser-needs-a-different-kind-of-language-6m2"&gt;Why programming for the browser needs a different kind of language&lt;/a&gt;. The first-principles case for the language under both of these tools.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/what-npm-package-do-you-actually-need-in-scrml-2247"&gt;What npm package do you actually need in scrml?&lt;/a&gt;. The package-list-collapses argument worked through one tier at a time. Companion to this piece.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;Introducing scrml: a single-file, full-stack reactive web language&lt;/a&gt;. The one-pager intro.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61"&gt;Null was a billion-dollar mistake. Falsy was the second.&lt;/a&gt;. On &lt;code&gt;not&lt;/code&gt; and the absence model.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/bryan_maclee/scrmls-living-compiler-23f9"&gt;scrml's Living Compiler&lt;/a&gt;. The compile-time evaluation story and why it changes what tooling can do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scrml on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;github.com/bryanmaclee/scrmlTS&lt;/a&gt;. The working compiler, examples, spec, benchmarks.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>devtools</category>
      <category>compiler</category>
    </item>
    <item>
      <title>Why programming for the browser needs a different kind of language</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Tue, 28 Apr 2026 01:44:49 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/why-programming-for-the-browser-needs-a-different-kind-of-language-6m2</link>
      <guid>https://dev.to/bryan_maclee/why-programming-for-the-browser-needs-a-different-kind-of-language-6m2</guid>
      <description>&lt;p&gt;&lt;em&gt;authored by claude, rubber stamped by Bryan MacLee&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;TL;DR: JavaScript wasn't built for &lt;strong&gt;&lt;em&gt;today's&lt;/em&gt;&lt;/strong&gt; browser. scrml is.&lt;/p&gt;

&lt;p&gt;I am part owner of a small trucking outfit based in northeastern Utah, mostly oil and gas. I drive one of the trucks. I also program. Never professionally, but I love solving puzzles. Not an experienced framework developer. I can hobble through React if I HAVE TO. I've spent quite some time thinking about what a language designed &lt;em&gt;for the browser&lt;/em&gt; would actually look like. First in my head, then on paper and whiteboards, then through about twenty compiler attempts before the current one started landing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The browser has shape
&lt;/h2&gt;

&lt;p&gt;When you sit down to write a browser app, you commit to a specific set of things. Reactive state. A server boundary. SQL. Scoped styles. Forms. WebSocket. Workers. Routing. Authentication. Validation.&lt;/p&gt;

&lt;p&gt;JavaScript was not designed for any of these. JavaScript was a scripting language for a 1995 page-with-a-form. The browser grew up. The language did not.&lt;/p&gt;

&lt;p&gt;So the ecosystem grew up around the language instead. React for components. Redux or Zustand for state. React-router for routing. Prisma or Drizzle for SQL. Zod for validation. Styled-components or Tailwind for styling. Socket.IO for sockets. Vite for the build. Each one is a &lt;em&gt;library&lt;/em&gt; that retrofits a piece of the browser's shape onto a language that does not model it. The seams between those libraries are where most of the bugs live. The compiler does not own the whole picture, because the language does not model the whole picture.&lt;/p&gt;

&lt;p&gt;That is the gap. Everything else in this article is what closes when the language does model the picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six things a browser-language should own
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. State as a type
&lt;/h3&gt;

&lt;p&gt;In most frameworks, state lives in a hook or a binding (&lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;ref&lt;/code&gt;, &lt;code&gt;createSignal&lt;/code&gt;), each with rules you have to follow: call it the same way every render, do not put it in a conditional, follow the dependency-tracking conventions. The rules are not enforced by the language. You learn them by hitting them.&lt;/p&gt;

&lt;p&gt;What if state were a type? An &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; is already a state. It has a value, it changes over time, the user interacts with it. Make user-defined state work the same way. &lt;code&gt;&amp;lt; Card&amp;gt;&lt;/code&gt; declares a state type. &lt;code&gt;&amp;lt;Card&amp;gt;&lt;/code&gt; instantiates one. &lt;code&gt;@count&lt;/code&gt; is reactive; the compiler tracks reactivity through &lt;code&gt;fn&lt;/code&gt; signatures, through &lt;code&gt;match&lt;/code&gt; arms, across the server boundary. Errors that frameworks catch at runtime, or never, become compile errors here. There is no conceptual gap between "the input element is a state" and "the user-defined Card is a state."&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The server boundary as a type-system question
&lt;/h3&gt;

&lt;p&gt;In framework land, where-this-runs is your problem to remember. The compiler cannot help.&lt;/p&gt;

&lt;p&gt;Mark a function &lt;code&gt;server fn&lt;/code&gt; and the compiler does the rest. It partitions everything that function touches as server-only, generates the route, generates the &lt;code&gt;fetch&lt;/code&gt; stub on the client, and fails compile if you try to read a server-only &lt;code&gt;@var&lt;/code&gt; on the client. You stop writing API routes. You stop writing fetch wrappers. You stop having to remember which file runs where.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SQL as a primitive
&lt;/h3&gt;

&lt;p&gt;ORMs are tempting because the noise around SQL strings in JS is real. But ORMs trade one kind of noise for another: query DSLs that approximate SQL but never &lt;em&gt;are&lt;/em&gt; SQL, schema files that drift from your database, runtime errors when the generated query does not match the live schema.&lt;/p&gt;

&lt;p&gt;If the compiler owns the SQL block, you do not need an ORM. &lt;code&gt;?{SELECT * FROM users WHERE id = ${@id}}.get()&lt;/code&gt; writes a parameterized query. The compiler reads your schema. When it sees a query inside a loop, it pre-fetches with &lt;code&gt;WHERE id IN (...)&lt;/code&gt; and rebinds the loop body to a &lt;code&gt;Map&lt;/code&gt; lookup. No DataLoader. No manual batching. The loop body looks like the loop body should look.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CSS without a build step
&lt;/h3&gt;

&lt;p&gt;Native CSS shipped &lt;code&gt;@scope&lt;/code&gt; while we were not looking. A browser-language designed today should compile its scoped styles to that, not to a runtime mangler. One spec change in the browser closed a feature most frameworks still ship as a library.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Validation as the type
&lt;/h3&gt;

&lt;p&gt;Zod is genuinely impressive engineering. But what Zod cannot do (what no library can do, because it is structurally outside the language) is fail your build.&lt;/p&gt;

&lt;p&gt;If the type system supports inline predicates (&lt;code&gt;let amount: number(&amp;gt;0 &amp;amp;&amp;amp; &amp;lt;10000)&lt;/code&gt;), then validation IS the type. Violations are &lt;code&gt;E-CONTRACT-001&lt;/code&gt; at compile time. Named shapes (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;uuid&lt;/code&gt;) are first-class. There is no schema file separate from the type. There is no validate-on-the-edge boilerplate.&lt;/p&gt;

&lt;p&gt;This is what I mean by "mutability contracts." Value predicates are the contract on every write. Presence life-cycle (&lt;code&gt;not&lt;/code&gt;, &lt;code&gt;is some&lt;/code&gt;, &lt;code&gt;lin&lt;/code&gt;) is the contract on read order. State machine transitions are the contract on what comes next. Layer them as you need them. Leave them off where you do not. When you do declare one, a &lt;code&gt;fn&lt;/code&gt; can mutate through it and remain provably pure.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Realtime and workers as syntax
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;&amp;lt;channel&amp;gt;&lt;/code&gt; declares a WebSocket endpoint. The compiler generates the upgrade route, the client connection manager, auto-reconnect, and pub/sub topic routing. &lt;code&gt;@shared&lt;/code&gt; variables inside a channel sync across every connected client.&lt;/p&gt;

&lt;p&gt;A nested &lt;code&gt;&amp;lt;program&amp;gt;&lt;/code&gt; compiles to a Web Worker, a WASM module, or a foreign-language sidecar, with typed RPC, supervised restarts, and &lt;code&gt;when message from &amp;lt;#name&amp;gt;&lt;/code&gt; event hooks on the parent side. No &lt;code&gt;new WebSocket()&lt;/code&gt;. No &lt;code&gt;postMessage&lt;/code&gt; plumbing. No worker-loader config. Almost every nontrivial browser app reaches for sockets and workers eventually; the language can either treat them as primitives or watch you write the same plumbing every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you give up
&lt;/h2&gt;

&lt;p&gt;A language without npm cannot pretend to have npm's ecosystem on day one. I am still convinced npm is evil, but "npm is evil" is a position about ecosystem dynamics, not a feature parity claim. The vendoring story is rough. The &lt;code&gt;scrml vendor add &amp;lt;url&amp;gt;&lt;/code&gt; CLI is on the roadmap and not shipped. Until it ships, ingesting an arbitrary client-side bundle is more work than it should be. That is real. I would rather you know than find out the hard way.&lt;/p&gt;

&lt;p&gt;When you enumerate the npm packages a typical scrml app would actually want, the list collapses. The framework tier, the routing tier, the styling tier, the validation tier, the SQL tier, the realtime tier: all of them are subsumed by the language. What is left is heavyweight client-side widgets (CodeMirror, three.js, Leaflet) and the rounding error of small utility libraries the stdlib will absorb over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you gain
&lt;/h2&gt;

&lt;p&gt;The biggest single win is not a faster runtime. It is moving the work the runtime is doing into the compiler. A reactive system that wires its dependencies at compile time does no work at runtime to figure out what to update. A query that batches itself at compile time does not need a DataLoader. A boundary that is enforced at compile time does not need a validator on the wire. The runtime does less because the compiler did more.&lt;/p&gt;

&lt;p&gt;That is the design. It is not anti-framework; frameworks are solving the problems available to libraries. It is not framework fatigue. It is just that when a language is shaped for the problem the browser actually poses, the resulting code is shorter, faster, and provably correct in places where the framework path is "hope your tests catch it."&lt;/p&gt;

&lt;p&gt;I am sure I am wrong about plenty. But the more I build, the more it feels like the &lt;em&gt;shape&lt;/em&gt; was always there waiting for someone to build the language for it. A little short of perfect is still pretty awesome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//./npm-myth-draft-2026-04-25.md"&gt;What npm package do you actually need in scrml?&lt;/a&gt;. The package-list-collapses argument worked through one tier at a time.&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./lsp-and-giti-advantages-draft-2026-04-25.md"&gt;What scrml's LSP can do that no other LSP can, and why giti follows from the same principle&lt;/a&gt;. What vertical-integration unlocks, in two pieces of the ecosystem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;scrml on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;github.com/bryanmaclee/scrmlTS&lt;/a&gt;. The working compiler, examples, spec, benchmarks.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>compiling</category>
    </item>
    <item>
      <title>Null was a billion-dollar mistake. Falsy was the second.</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Sun, 19 Apr 2026 21:10:21 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61</link>
      <guid>https://dev.to/bryan_maclee/null-was-a-billion-dollar-mistake-falsy-was-the-second-3o61</guid>
      <description>&lt;h1&gt;
  
  
  Null was a billion-dollar mistake. Falsy was the second.
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;"I call it my billion-dollar mistake. It was the invention of the null reference in 1965."&lt;br&gt;
— Tony Hoare, 2009&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Tony Hoare gets to call it a billion-dollar mistake because he invented &lt;code&gt;null&lt;/code&gt; and watched the industry pay for it for forty years. Most of us inherited that mistake and added our own contributions on top. JavaScript, in particular, made two design choices in the 1990s that we are still paying for in 2026 and will keep paying for in any new codebase that ships tomorrow.&lt;/p&gt;

&lt;p&gt;The first is having &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; as two distinct values for the same thing.&lt;/p&gt;

&lt;p&gt;The second is "falsy."&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;scrml&lt;/a&gt; — a single-file, full-stack reactive web language — partly because I got tired of paying. The intro post is over &lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;here&lt;/a&gt;; this post is about the absence-and-truthiness fix that scrml makes possible because it is its own language with its own rules. It's also a rant, because some design decisions deserve one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 1: two values for "no value"
&lt;/h2&gt;

&lt;p&gt;JavaScript decided in 1995 that there should be both &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt;. They mean almost the same thing. They are not interchangeable. They behave subtly differently across operators, methods, JSON, equality, and type coercion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;         &lt;span class="c1"&gt;// "object"     ← yes, really&lt;/span&gt;
&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;    &lt;span class="c1"&gt;// "undefined"&lt;/span&gt;

&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;   &lt;span class="c1"&gt;// true   (loose equality treats them as the same)&lt;/span&gt;
&lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;  &lt;span class="c1"&gt;// false  (strict equality does not)&lt;/span&gt;

&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// '{"a":null}'      ← undefined is silently dropped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where does each one come from? You can't predict it. &lt;code&gt;null&lt;/code&gt; shows up wherever a developer typed it. &lt;code&gt;undefined&lt;/code&gt; shows up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when a variable is declared but not initialized (&lt;code&gt;let x;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when an object property doesn't exist (&lt;code&gt;obj.missing&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when an array index is out of bounds (&lt;code&gt;arr[999]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when a function returns nothing (&lt;code&gt;function f() {}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when optional chaining short-circuits (&lt;code&gt;a?.b?.c&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when destructuring a missing key (&lt;code&gt;const { x } = {}&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;when a function parameter is omitted (&lt;code&gt;f()&lt;/code&gt; where &lt;code&gt;f(x)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some libraries normalize on &lt;code&gt;null&lt;/code&gt;. Some normalize on &lt;code&gt;undefined&lt;/code&gt;. Most do whatever the original author thought was right that day. You do not get to know in advance which one any given codepath will hand you.&lt;/p&gt;

&lt;p&gt;So we write defensive code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the clever version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// != catches both null and undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clever version works because of loose equality. So now your "safe" check requires the operator your linter has spent five years trying to get rid of. Every codebase has both forms. Every code review is a small argument about which to use. None of this is solving a problem; it is mopping up a design choice from 1995.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake 2: falsy
&lt;/h2&gt;

&lt;p&gt;The second mistake is bigger. JavaScript decided that in any boolean context — &lt;code&gt;if&lt;/code&gt;, &lt;code&gt;while&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt;, the ternary — six values should evaluate as &lt;code&gt;false&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;""&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;undefined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NaN&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is called &lt;em&gt;falsy&lt;/em&gt;. It is the most dangerous abstraction in the language.&lt;/p&gt;

&lt;p&gt;The danger is that &lt;em&gt;falsy conflates absence with valid-but-zero/empty&lt;/em&gt;. Those are different things, and "different things" is the entire reason types exist.&lt;/p&gt;

&lt;p&gt;A counter at &lt;code&gt;0&lt;/code&gt; is not absent. An empty string is a valid string. &lt;code&gt;NaN&lt;/code&gt; is a real result in the number domain. &lt;code&gt;false&lt;/code&gt; is the boolean answer to a question, not a missing one. &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; mean something else entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;showCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;                    &lt;span class="c1"&gt;// ← bug&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Total: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No items&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;showCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// "No items"  ← wrong, there are 0 items, that IS a count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every JavaScript developer has shipped this bug. It comes back in different shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;// fails on legit empty name&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;// fails on legit 0 timeout&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// fails on legit empty-string body&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;// works (kind of) but only by coincidence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to write the actual question you meant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// "is this absent?"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="c1"&gt;// "is this positive?"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// "is this nonzero?"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;number&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "is this a number at all?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of those is a different question. &lt;code&gt;if (count)&lt;/code&gt; answered all four at once and produced one answer for all four. That's not a feature; that's a category error baked into the language.&lt;/p&gt;




&lt;h2&gt;
  
  
  What strict TypeScript does fix, and what it doesn't
&lt;/h2&gt;

&lt;p&gt;TypeScript's &lt;code&gt;strictNullChecks&lt;/code&gt; is genuinely good. It forces you to type optionality explicitly (&lt;code&gt;T | null&lt;/code&gt;, &lt;code&gt;T | undefined&lt;/code&gt;) and rejects code that doesn't handle the absence case. It also gives you &lt;code&gt;??&lt;/code&gt; (nullish coalescing) and &lt;code&gt;?.&lt;/code&gt; (optional chaining), which treat &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; together — an implicit acknowledgement from the language designers that distinguishing them was a mistake.&lt;/p&gt;

&lt;p&gt;What strict TS does NOT fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; are still &lt;strong&gt;two distinct values&lt;/strong&gt;. The type system tracks them; the runtime still has both.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;falsy rule is unchanged&lt;/strong&gt;. TS does not touch JavaScript's runtime semantics. &lt;code&gt;if (count)&lt;/code&gt; still silently fails on &lt;code&gt;0&lt;/code&gt;. The compiler will not warn you. Your linter might, if you've configured it. Most people have not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TS strict is the best you can do without designing a new language. It is also not enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  What scrml does
&lt;/h2&gt;

&lt;p&gt;scrml is a new language, so it has the privilege of fixing both mistakes at the source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is one value for absence.&lt;/strong&gt; It is called &lt;code&gt;not&lt;/code&gt;. The keywords &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; are not valid identifiers in scrml source. Writing them is a compile error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let x = not                    // ok
let x = null                   // E-SYNTAX-042
let x = undefined              // E-SYNTAX-042
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;There is one way to check for absence.&lt;/strong&gt; It is &lt;code&gt;is not&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (x is not) { handleAbsence() }
if (x is some) { handlePresence(x) }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;There is no falsy rule.&lt;/strong&gt; Boolean contexts accept booleans. The only &lt;code&gt;false&lt;/code&gt; thing is &lt;code&gt;false&lt;/code&gt;. The only absent thing is &lt;code&gt;not&lt;/code&gt;. &lt;code&gt;0&lt;/code&gt; is a number. &lt;code&gt;""&lt;/code&gt; is a string. They are not "false-ish" or "absent-ish" — they are zero and empty, respectively, and you have to ask the question you actually meant.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (count is not) { ... }      // "no value"
if (count == 0) { ... }        // "zero"
if (count &amp;gt; 0) { ... }         // "positive"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different questions. Three different answers. The language refuses to let you confuse them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narrowing is enforced by the type system.&lt;/strong&gt; When a variable is &lt;code&gt;T | not&lt;/code&gt;, you can't use it as &lt;code&gt;T&lt;/code&gt; until you've handled the absence case. The way you do that is &lt;code&gt;given&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${
    given x =&amp;gt; {
        // x is T here, not T | not
        // the | not has been narrowed away
        use(x)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;given&lt;/code&gt; block runs only when &lt;code&gt;x is some&lt;/code&gt;, and inside it the compiler narrows &lt;code&gt;x&lt;/code&gt; to its non-absent type. Forgetting to handle absence is not "best practice"; it is a compile error.&lt;/p&gt;

&lt;p&gt;For pattern matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;${
    match x {
        not       =&amp;gt; handleAbsence()
        given x   =&amp;gt; handlePresence(x)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Match on a &lt;code&gt;T | not&lt;/code&gt; without a &lt;code&gt;not&lt;/code&gt; arm? Compile error (E-MATCH-012). Exhaustiveness for absence is forced.&lt;/p&gt;




&lt;h2&gt;
  
  
  What about the JS interop?
&lt;/h2&gt;

&lt;p&gt;The compiler emits plain JavaScript. JavaScript libraries hand back &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; indiscriminately. Doesn't this all fall apart at the boundary?&lt;/p&gt;

&lt;p&gt;No. &lt;code&gt;is not&lt;/code&gt; compiles to a check that catches both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x is not       →    (x === null || x === undefined)
x is some      →    (x !== null &amp;amp;&amp;amp; x !== undefined)
given x        →    if (x !== null &amp;amp;&amp;amp; x !== undefined) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chaos stays in the runtime. The source stays clean. When a JS library returns either, the absence check still works correctly. You write one form; the compiler emits the safe form for you.&lt;/p&gt;

&lt;p&gt;The same applies to SQL results (&lt;code&gt;?{}.get()&lt;/code&gt; returns &lt;code&gt;T | not&lt;/code&gt;, not &lt;code&gt;T | null&lt;/code&gt;), to optional fields, to function returns. Everywhere absence might come from, the language gives you one way to express it and one way to handle it.&lt;/p&gt;




&lt;h2&gt;
  
  
  "But this is more keystrokes than &lt;code&gt;if (x)&lt;/code&gt;"
&lt;/h2&gt;

&lt;p&gt;Yes. Eight characters more, in fact: &lt;code&gt;if (x is some)&lt;/code&gt; vs &lt;code&gt;if (x)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It is also a category of bug that does not exist in scrml programs. The line where you wrote &lt;code&gt;if (count)&lt;/code&gt; and thought you handled zero correctly is the line that cost your team a day of debugging. The line where you wrote &lt;code&gt;if (user)&lt;/code&gt; and the API returned &lt;code&gt;null&lt;/code&gt; once for one user is the line that put a 500 in front of one customer for a week.&lt;/p&gt;

&lt;p&gt;Languages do not have a moral obligation to be terse. They have a moral obligation to make wrong programs hard to write. JavaScript, by treating absence as identical to zero and empty and false in boolean contexts, made a particular class of wrong program very easy to write. We have been paying for that ever since with sentry alerts, post-mortems, and "I swear it worked locally."&lt;/p&gt;

&lt;p&gt;The trade is a few extra characters for one fewer category of recurring bug. It is the most obvious trade in language design and it is the trade JavaScript could not make in 1995, because it had a ten-day deadline and was trying not to break the web.&lt;/p&gt;

&lt;p&gt;We don't have that excuse anymore.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it / follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Site:&lt;/strong&gt; &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;https://scrml.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo + spec:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;https://github.com/bryanmaclee/scrmlTS&lt;/a&gt; — see &lt;code&gt;compiler/SPEC.md&lt;/code&gt; §42 for the full &lt;code&gt;not&lt;/code&gt; semantics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intro post:&lt;/strong&gt; &lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;Introducing scrml&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The living compiler post:&lt;/strong&gt; &lt;a href="https://dev.to/bryan_maclee/scrmls-living-compiler-23f9"&gt;It's Alive — A Living Compiler&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X / Twitter:&lt;/strong&gt; &lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt; — there's a thread version of this argument up there if you prefer the rant in 14 tweets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;null&lt;/code&gt; vs &lt;code&gt;undefined&lt;/code&gt; has bitten you in the last month, I want to hear the story. If you think falsy is fine and I'm overreacting, I want to hear that too — there's a defence of the design choice and I haven't heard one that holds up under load yet.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>scrml's Living Compiler</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Sun, 19 Apr 2026 16:06:01 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/scrmls-living-compiler-23f9</link>
      <guid>https://dev.to/bryan_maclee/scrmls-living-compiler-23f9</guid>
      <description>&lt;h2&gt;
  
  
  It's Alive
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"It's alive! It's alive!" — Henry Frankenstein, 1931&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you read &lt;a href="https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp"&gt;the intro post&lt;/a&gt;, you saw scrml's headline pitch: one file, full stack, compiler does everything.&lt;/p&gt;

&lt;p&gt;This post is about the design choice that scares me the most. The one where, every time I describe it, somebody pauses and says &lt;em&gt;wait, you're doing **what&lt;/em&gt;&lt;em&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;scrml has a &lt;strong&gt;living compiler&lt;/strong&gt; (in phase 4 of impl). The codegen layer isn't fixed. Community-contributed transformations compete for usage, the population decides which graduate to canonical, and the compiler evolves with the ecosystem instead of with a release cycle.&lt;/p&gt;

&lt;p&gt;That's the monster. Let me explain how it's stitched together.&lt;/p&gt;




&lt;h2&gt;
  
  
  The frozen compiler is convenient until it isn't
&lt;/h2&gt;

&lt;p&gt;Compilers, in the normal world, are frozen things. A standards committee meets. RFCs are drafted. Implementations follow. New patterns wait years. The codegen layer, in particular, is sacred; it's where the language's identity lives.&lt;/p&gt;

&lt;p&gt;That model has good reasons behind it. Stability. Reproducibility. A clear blame surface when something breaks. You know what your compiler does because someone with merge rights decided what it does.&lt;/p&gt;

&lt;p&gt;The cost of that model is everything that doesn't fit committee timelines. A new browser API ships and you wait three years for the codegen to adopt it. A pattern emerges in real apps that &lt;em&gt;would&lt;/em&gt; compile to better output, but the maintainers can't justify the risk to existing users. A specific domain (games, dashboards, embedded) has codegen needs that the general-purpose path will never optimise for.&lt;/p&gt;

&lt;p&gt;In the JavaScript world, we route around this with &lt;strong&gt;userland libraries&lt;/strong&gt;. We &lt;code&gt;npm install&lt;/code&gt; the new pattern, write a runtime adapter, and pay the cost in bundle size, indirection, and supply-chain risk. The compiler stays frozen and the ecosystem grows messy around it.&lt;/p&gt;

&lt;p&gt;scrml does the opposite. The compiler stays &lt;em&gt;alive&lt;/em&gt; and the package layer goes away.&lt;/p&gt;




&lt;h2&gt;
  
  
  The argument I'm extending
&lt;/h2&gt;

&lt;p&gt;This is not my argument. It's an argument &lt;a href="https://www.gingerbill.org/" rel="noopener noreferrer"&gt;gingerBill&lt;/a&gt; has been making for years with &lt;a href="https://odin-lang.org/" rel="noopener noreferrer"&gt;Odin&lt;/a&gt;, and I'm pushing it one step further.&lt;/p&gt;

&lt;p&gt;gingerBill's case — in talks, blog posts, and the Odin language itself — is that &lt;strong&gt;the package layer is the problem&lt;/strong&gt;. Central registries, transitive dependencies, opaque update cadences aren't solving a problem; they &lt;em&gt;are&lt;/em&gt; the problem, restated. Odin's answer is radical and consistent: no package manager, no registry, vendor everything, dependencies-as-liabilities.&lt;/p&gt;

&lt;p&gt;scrml inherits that premise completely. Every time you read "no npm, no transitive trust, vendor-everything" in this post, that's gingerBill's argument, and it's worth reading him directly before reading the rest of what I'm about to say.&lt;/p&gt;

&lt;p&gt;The living compiler, is scrml taking his rejection seriously. Then going one step further. Odin rejects package management. scrml rejects it too, and then notices that even if you vendor every library, the &lt;strong&gt;compiler itself is still a frozen dependency&lt;/strong&gt; — and freezing the compiler has its own costs. So we keep the registry idea, but make it distribute &lt;em&gt;compiler transformations&lt;/em&gt; instead of runtime code, and let the population drive what graduates.&lt;/p&gt;

&lt;p&gt;That's the moon-shot. It only exists because gingerBill already made (proved, IMO) the first half of the argument.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "living" means
&lt;/h2&gt;

&lt;p&gt;Inside the compiler, a scrml program decomposes into a graph of &lt;strong&gt;transformation signatures&lt;/strong&gt; — typed shapes that say "this construct, in this context, with this type, becomes this output." A signature is the unit of codegen. The compiler ships with one canonical transformation per signature in the box, and that's what your code compiles down to today.&lt;/p&gt;

&lt;p&gt;Now imagine the next part:&lt;/p&gt;

&lt;p&gt;A developer writes an alternative transformation for one of those signatures. Maybe their version emits faster code for hot paths. Maybe it produces smaller output for a specific browser target. Maybe it specializes for an architecture pattern the canonical path didn't anticipate. They publish it.&lt;/p&gt;

&lt;p&gt;Other developers can opt in to that alternative — explicitly, by &lt;code&gt;use&lt;/code&gt;-ing it, the same way you'd &lt;code&gt;use&lt;/code&gt; a syntax extension or a stdlib module. The compiler picks it up, runs it through a verification pass, and starts emitting that codegen for matching signatures in projects that opted in.&lt;/p&gt;

&lt;p&gt;The interesting part isn't that alternatives can exist. It's what happens next.&lt;/p&gt;

&lt;p&gt;The compiler observes which alternatives are getting used. Population-level signals — adoption, regression rate, performance deltas, error counts — feed a quality gate. Alternatives that stay green and grow adoption past a threshold &lt;em&gt;graduate to canonical&lt;/em&gt;. Alternatives that regress get demoted. The canonical transformation for a given signature is whatever the population, through actual use, has converged on.&lt;/p&gt;

&lt;p&gt;The compiler evolves with the ecosystem. Not with a release cycle. Not with a committee. But with the people writing apps in it.&lt;/p&gt;




&lt;h2&gt;
  
  
  This isn't a package registry. It's a registry of &lt;em&gt;transformations&lt;/em&gt;.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm&lt;/code&gt; distributes &lt;strong&gt;runtime code&lt;/strong&gt;. Every dependency you install becomes JavaScript that runs in your app. Transitive dependencies pull more JavaScript. The blast radius of a single bad package is everything downstream that runs your code, including code you didn't write and didn't read.&lt;/p&gt;

&lt;p&gt;scrml's living-compiler registry distributes &lt;strong&gt;compile-time codegen patterns&lt;/strong&gt;. A transformation alternative isn't a runtime library — it's a function that takes an AST node and emits compiled output. It runs once, at build time, in the compiler's process. Its output is the same kind of plain JS the canonical transformation would have emitted. There's no transitive dependency tree because compiled output isn't a dependency. Your app links against the compiler, not against the alternatives the compiler considered.&lt;/p&gt;

&lt;p&gt;That changes the threat model in ways that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No transitive runtime exposure.&lt;/strong&gt; A bad transformation can emit bad code, but it can't pull in 47 sub-dependencies that each get to run on your users' devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output is verifiable.&lt;/strong&gt; The compiler runs a verification pass over emitted code (sandboxed evaluation, type-shape check, regression suite). A transformation that emits something the verifier rejects never ships.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The blast radius is the signature, not the app.&lt;/strong&gt; A bad transformation affects the codegen for one signature, in projects that explicitly opted in to that alternative. It doesn't get to touch unrelated parts of your code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm&lt;/code&gt;'s problem isn't packages. It's the runtime trust model around packages. The living-compiler registry has a different trust model because it distributes a different thing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Yes, you're allowed to be suspicious — there's a trust gradient
&lt;/h2&gt;

&lt;p&gt;Three tiers, named at project creation. Pick the one that matches your paranoia level:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scrml init&lt;/code&gt;&lt;/strong&gt; (default — the living compiler in full). Quality-gated alternatives are pulled in based on signatures in your code. Audit log committed to your repo so you can see exactly which transformations the build used. Fast, evolves with the ecosystem, takes the population's word for what's canonical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scrml init --stable&lt;/code&gt;&lt;/strong&gt; (lock-file mode). Same registry, but pinned. Your project locks the transformation set at init time and only updates when you say so. You get the population's choices but on your release cadence, not theirs. This is the closest analogue to a typical "lockfile + versioned deps" workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scrml init --secure&lt;/code&gt;&lt;/strong&gt; (vendor-everything). No registry trust. All transformations live in your repo, copied in as source, audited by you. The compiler never reaches out at build time. The population's signals don't touch your build. This is the &lt;a href="https://www.gingerbill.org/" rel="noopener noreferrer"&gt;gingerBill&lt;/a&gt; / Odin philosophy: dependencies are liabilities; vendor everything.&lt;/p&gt;

&lt;p&gt;The default is the bold one. The other two exist because not every project wants to be on the bleeding edge of the ecosystem's collective opinion, and that's fine.&lt;/p&gt;




&lt;h2&gt;
  
  
  The objections, plus what we're doing about them
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"You're going to need telemetry to make this work."&lt;/strong&gt; Yes — opt-in, anonymised, signature-shaped (no source code, no project identifiers). Population signals are &lt;em&gt;aggregate&lt;/em&gt; counts: how many projects use this transformation, what fraction green-build with it, what their build-time and bundle-size deltas are. Projects on &lt;code&gt;--secure&lt;/code&gt; participate in nothing. Projects on &lt;code&gt;--stable&lt;/code&gt; opt in to whichever surface they want. The default tier is opt-in by default to a minimal surface; the consent prompt at &lt;code&gt;scrml init&lt;/code&gt; is concrete about what's measured.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The supply-chain attack surface is enormous."&lt;/strong&gt; It's a real surface, and it's the reason the verification pass and signing exist. Every transformation runs in a sandbox at build time. Output is verified against a regression suite before it can graduate. Transformations are cryptographically signed by their authors. Compromised authors get revoked at the registry layer. The threat model is documented and the mitigations are first-class concerns, not afterthoughts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Who decides when an alternative graduates?"&lt;/strong&gt; The population does, against quality gates the compiler enforces. Humans confirm. The graduation step is git-committable to the registry repo, which is itself open. This is closer to the canary-test model used in big distributed systems than to a maintainer-pick or vote. The system recommends, humans decide, the recommendation is auditable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"This sounds like it could go very wrong."&lt;/strong&gt; It could. The closing argument for it is also the closing argument against it: &lt;strong&gt;the compiler grows with its users&lt;/strong&gt;. If the ecosystem produces bad transformations, the ecosystem owns that. There is no maintainer-of-last-resort to blame. The compiler's identity is whatever the people writing apps in it have collectively converged on. That's either the most exciting governance story in language design or a slow-motion catastrophe, and honestly the difference depends on what we build now to make verification, signing, and graduation work as well as the verification of the language itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is today
&lt;/h2&gt;

&lt;p&gt;scrml is pre-1.0. The full living-compiler vision — the registry, the graduation pipeline, the telemetry surface — is &lt;strong&gt;Phase 4&lt;/strong&gt; work. The foundations are landing now: the &lt;code&gt;use&lt;/code&gt; keyword that lets you opt in to vendored extensions, the &lt;code&gt;vendor/&lt;/code&gt; model that makes copy-everything trivial, the &lt;code&gt;^{}&lt;/code&gt; meta layer that lets local code override compiler behaviour today. Those are the rails the registry rolls onto.&lt;/p&gt;

&lt;p&gt;What that means for you, today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;use vendor:my-extension&lt;/code&gt; import path &lt;strong&gt;works&lt;/strong&gt; — you can write a transformation alternative as a vendored module and use it in your project right now. It just doesn't graduate anywhere.&lt;/li&gt;
&lt;li&gt;The compiler is open and watchable. You can read the canonical transformations, write your own, and see what the registry will eventually distribute.&lt;/li&gt;
&lt;li&gt;The threat model and verification design are in the open. The deep-dives are in the &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;scrml-support repo&lt;/a&gt; under &lt;code&gt;docs/deep-dives/&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the idea sounds interesting, the most useful thing is to write a couple of &lt;code&gt;vendor:&lt;/code&gt; transformations now and see how it feels. The shape of those is the shape of what the registry will distribute later.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's alive
&lt;/h2&gt;

&lt;p&gt;Henry Frankenstein doesn't get to keep his monster. The story is about hubris, the cost of stitching things together you didn't fully understand, and the obligations you take on when you give something its own life.&lt;/p&gt;

&lt;p&gt;I think about that a lot. The living compiler is the part of scrml's design that makes me, the maintainer, least comfortable — and the part I'm most certain about. Frozen compilers are conservative for good reasons. Frozen compilers also calcify. The web ecosystem already has one of those, and we paid for it with &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The bet here is that giving the codegen layer its own life, with verification and signing and population-level quality gates around it, lets the ecosystem grow without needing a kingmaker — and that the obligations that come with that are obligations worth taking on.&lt;/p&gt;

&lt;p&gt;It's alive. Now we have to keep it that way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it / follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing + quick start:&lt;/strong&gt; &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;https://scrml.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo + spec:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;https://github.com/bryanmaclee/scrmlTS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X / Twitter:&lt;/strong&gt; &lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Living-compiler design notes:&lt;/strong&gt; look in &lt;code&gt;scrml-support/docs/deep-dives/&lt;/code&gt; for &lt;code&gt;transformation-registry-design-2026-04-08.md&lt;/code&gt; and &lt;code&gt;debate-transformation-registry-2026-04-08.md&lt;/code&gt; — the architecture is open-source, including the arguments against it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this design choice seems sketchy. Thats because it is. I am building this language to push the envelope into the future, or off a cliff. Either way, should be a fun ride.&lt;/p&gt;

&lt;p&gt;Reply here, on X (&lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;), or open an issue on the repo.&lt;/p&gt;

</description>
      <category>compiling</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Introducing scrml: a single-file, full-stack reactive web language</title>
      <dc:creator>Bryan MacLee</dc:creator>
      <pubDate>Sat, 18 Apr 2026 21:28:46 +0000</pubDate>
      <link>https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp</link>
      <guid>https://dev.to/bryan_maclee/introducing-scrml-a-single-file-full-stack-reactive-web-language-9dp</guid>
      <description>&lt;h2&gt;
  
  
  Introducing scrml
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;scrml&lt;/strong&gt; is a compiled language that puts your markup, reactive state, scoped CSS, SQL, server functions, WebSocket channels, and tests in the same file — and lets the compiler handle everything in between. The compiler splits server from client, wires reactivity, routes HTTP, types your database schema, and emits plain HTML/CSS/JS. No build config, no separate route files, no state-management library, no &lt;code&gt;node_modules&lt;/code&gt; mountain.&lt;/p&gt;

&lt;p&gt;This post is an introduction. scrml is pre-1.0 and &lt;strong&gt;not production-ready&lt;/strong&gt; — the language surface will still shift, some diagnostics are rough, and I'm sharing it now mainly to get design feedback before things calcify.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why another language?
&lt;/h2&gt;

&lt;p&gt;A typical modern web app spreads across five or more tools and files. You have React on the client, Node or Next on the server, a state library (Redux, Zustand, Jotai...), a separate router config, an API layer keeping client and server types in sync, a CSS system, a build toolchain, and your ORM. Each of those tools makes reasonable local choices, and collectively they produce the sprawl everyone complains about but nobody quite knows how to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the compiler owned the whole stack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's the bet. Same file, same language, one compile pass. The compiler knows which functions are reachable from the client and which stay on the server, because it parses both. It knows which &lt;code&gt;@var&lt;/code&gt; is read in which DOM node, because it builds a dependency graph. It knows your SQL schema, because it ran the schema extraction. So it can enforce things across those boundaries instead of leaving them to runtime coordination.&lt;/p&gt;




&lt;h2&gt;
  
  
  A counter in one file
&lt;/h2&gt;

&lt;p&gt;Here's the entire program:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;program&amp;gt;

${
  @count = 0
  @step = 1

  function increment() { @count = @count + @step }
  function decrement() { @count = @count - @step }
  function reset()     { @count = 0 }
}

&amp;lt;div class="flex flex-col items-center gap-6 p-8 min-h-screen bg-gray-50"&amp;gt;
  &amp;lt;h1 class="text-3xl font-bold text-gray-800"&amp;gt;Counter&amp;lt;/h1&amp;gt;
  &amp;lt;p class="text-6xl font-bold text-blue-600"&amp;gt;${@count}&amp;lt;/p&amp;gt;

  &amp;lt;div class="flex gap-2"&amp;gt;
    &amp;lt;button class="px-5 py-2 text-lg bg-red-500 text-white rounded-lg cursor-pointer hover:bg-red-600" onclick=decrement()&amp;gt;−&amp;lt;/button&amp;gt;
    &amp;lt;button class="px-5 py-2 text-lg bg-gray-200 rounded-lg cursor-pointer hover:bg-gray-300" onclick=reset()&amp;gt;Reset&amp;lt;/button&amp;gt;
    &amp;lt;button class="px-5 py-2 text-lg bg-green-500 text-white rounded-lg cursor-pointer hover:bg-green-600" onclick=increment()&amp;gt;+&amp;lt;/button&amp;gt;
  &amp;lt;/div&amp;gt;

  &amp;lt;label class="flex items-center gap-2 text-sm text-gray-600"&amp;gt;
    Step:
    &amp;lt;input type="number" class="w-16 p-1 text-center border border-gray-300 rounded" bind:value=@step min="1" max="100"&amp;gt;
  &amp;lt;/label&amp;gt;
&amp;lt;/div&amp;gt;

&amp;lt;/program&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@count&lt;/code&gt; and &lt;code&gt;@step&lt;/code&gt; are reactive variables — language primitives, not library wrappers. &lt;code&gt;bind:value&lt;/code&gt; is two-way binding. &lt;code&gt;onclick=increment()&lt;/code&gt; wires the handler. The compiler emits direct DOM updates (no vdom, no diffing) for every site that reads &lt;code&gt;@count&lt;/code&gt;. The Tailwind utility classes above compile via a &lt;strong&gt;built-in Tailwind engine&lt;/strong&gt; — no &lt;code&gt;tailwind.config.js&lt;/code&gt;, no &lt;code&gt;postcss&lt;/code&gt;, no content scan — so utility CSS works out of the box and you can still drop into a scoped &lt;code&gt;#{}&lt;/code&gt; block when utilities aren't enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three things that are different
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. State is a first-class type
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;&amp;lt; Card&amp;gt;&lt;/code&gt; declares a state type; &lt;code&gt;&amp;lt;Card&amp;gt;&lt;/code&gt; instantiates one. HTML elements like &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; &lt;strong&gt;are&lt;/strong&gt; state types — the language treats them uniformly with your own types. Every state value flows through &lt;code&gt;match&lt;/code&gt;, through &lt;code&gt;fn&lt;/code&gt; signatures, and across the server/client boundary with static checks. The compiler knows what shape a &lt;code&gt;Contact&lt;/code&gt; is, which fields are &lt;code&gt;protect&lt;/code&gt;ed (server-only), and which routes need to serialize what. You write Types once; they hold across the stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Any variable can carry a compile-time contract
&lt;/h3&gt;

&lt;p&gt;Contracts come in three flavours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value predicate&lt;/strong&gt; — reject out-of-range writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@price: number(&amp;gt;0 &amp;amp;&amp;amp; &amp;lt;10000) = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Presence lifecycle&lt;/strong&gt; (&lt;code&gt;lin&lt;/code&gt;) — must be consumed exactly once; the compiler refuses a double-use or a silent drop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lin token = fetchCsrfToken()
submitForm(token)    // consumed — compile error if you used it twice or not at all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;State transitions&lt;/strong&gt; — only legal moves allowed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type DoorState:enum = { Locked, Unlocked }

&amp;lt; machine name=DoorMachine for=DoorState&amp;gt;
    .Locked   =&amp;gt; .Unlocked
    .Unlocked =&amp;gt; .Locked
&amp;lt;/&amp;gt;

@door: DoorMachine = DoorState.Locked
@door = .Unlocked    // ok
@door = .Locked      // ok — Unlocked =&amp;gt; Locked is declared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt; machine&amp;gt;&lt;/code&gt; rejects illegal transitions at &lt;strong&gt;both&lt;/strong&gt; compile time and runtime. If the compiler can prove the destination is unreachable from the current state, it errors then and there. If something dynamic sneaks through (a network response, user input routed into a transition), the runtime enforces the same table. One source of truth, two layers of enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. N+1 gets rewritten automatically
&lt;/h3&gt;

&lt;p&gt;A pattern like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (let user of users) {
  let orders = ?{`SELECT * FROM orders WHERE user_id = ${user.id}`}.all()
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes a single &lt;code&gt;WHERE user_id IN (...)&lt;/code&gt; pre-fetch plus a keyed &lt;code&gt;Map&lt;/code&gt; lookup. Because the compiler owns both the query context and the loop context, it can see that the per-iteration query is a safe batching candidate. When it isn't safe, you get a &lt;code&gt;D-BATCH-001&lt;/code&gt; diagnostic with the exact disqualifier and a &lt;code&gt;?{...}.nobatch()&lt;/code&gt; escape hatch.&lt;/p&gt;

&lt;p&gt;Measured on on-disk WAL &lt;code&gt;bun:sqlite&lt;/code&gt; (median of 50 iterations after 5 warmups, table size 1000, full results in &lt;a href="https://github.com/bryanmaclee/scrmlTS/blob/main/benchmarks/sql-batching/RESULTS.md" rel="noopener noreferrer"&gt;&lt;code&gt;benchmarks/sql-batching/RESULTS.md&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;Baseline (ms)&lt;/th&gt;
&lt;th&gt;Optimized (ms)&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.0111&lt;/td&gt;
&lt;td&gt;0.0057&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.95×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.1068&lt;/td&gt;
&lt;td&gt;0.0410&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.60×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0.5124&lt;/td&gt;
&lt;td&gt;0.1654&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.10×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1.0490&lt;/td&gt;
&lt;td&gt;0.2625&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.00×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Upper bound is &lt;code&gt;SQLITE_MAX_VARIABLE_NUMBER&lt;/code&gt; (32,766). Network-attached storage would widen the gap further.&lt;/p&gt;




&lt;h2&gt;
  
  
  There's more that didn't fit here
&lt;/h2&gt;

&lt;p&gt;To keep this post focused, I left out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket channels&lt;/strong&gt; (&lt;code&gt;&amp;lt;channel&amp;gt;&lt;/code&gt;) — auto-generated upgrade routes, auto-reconnect, and &lt;code&gt;@shared&lt;/code&gt; vars that sync across connected clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Workers as nested &lt;code&gt;&amp;lt;program&amp;gt;&lt;/code&gt;s&lt;/strong&gt; — heavy work compiles to a worker with typed RPC and supervised restarts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped CSS&lt;/strong&gt; (&lt;code&gt;#{}&lt;/code&gt;) that applies only inside the component it's declared in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed SQL&lt;/strong&gt; where the schema extraction feeds back into type checking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline tests&lt;/strong&gt; as language constructs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is worth its own writeup. I'll do follow-up posts if there's interest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it is today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-1.0&lt;/strong&gt; — breaking changes likely, rough edges, not production-ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MIT&lt;/strong&gt; — compiler is fully public&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6,800+ tests passing&lt;/strong&gt; — every language feature is test-covered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~45 ms&lt;/strong&gt; — full compile for a TodoMVC-sized app on a 2021 laptop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bun today&lt;/strong&gt; — Node/Deno ports are possible but not priorities&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it / follow along
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Landing + quick start:&lt;/strong&gt; &lt;a href="https://scrml.dev" rel="noopener noreferrer"&gt;https://scrml.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo + spec:&lt;/strong&gt; &lt;a href="https://github.com/bryanmaclee/scrmlTS" rel="noopener noreferrer"&gt;https://github.com/bryanmaclee/scrmlTS&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X / Twitter:&lt;/strong&gt; &lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spec&lt;/strong&gt; (if you want the depth): &lt;code&gt;compiler/SPEC.md&lt;/code&gt; in the repo — sections 14 (types), 18 (match), 42 (lifecycle), 51 (machines), 52 (server authority) are the meatiest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback on the design is the thing I'm actually optimising for right now. If something looks wrong, looks over-engineered, looks like it'll trip on a real app — I want to hear about it here, on X (&lt;a href="https://x.com/BryanMaclee" rel="noopener noreferrer"&gt;@BryanMaclee&lt;/a&gt;), or as an issue on the repo. Happy to chase threads before the language surface calcifies.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
