<?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: YQteam</title>
    <description>The latest articles on DEV Community by YQteam (@yqteamdyq).</description>
    <link>https://dev.to/yqteamdyq</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4109553%2F6063d7ea-7520-4225-b869-ac2d91352fa3.png</url>
      <title>DEV Community: YQteam</title>
      <link>https://dev.to/yqteamdyq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yqteamdyq"/>
    <language>en</language>
    <item>
      <title>yq-sanyi v0.4.0: define a component once, use it as a real HTML tag anywhere</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Mon, 05 Oct 2026 14:34:28 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/yq-sanyi-v040-define-a-component-once-use-it-as-a-real-html-tag-anywhere-3iih</link>
      <guid>https://dev.to/yqteamdyq/yq-sanyi-v040-define-a-component-once-use-it-as-a-real-html-tag-anywhere-3iih</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1g62q47kitf0z5a62xz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1g62q47kitf0z5a62xz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yq-sanyi&lt;/code&gt; is a web component framework with no runtime dependencies. You define a component once, and after that it is a native HTML element you can use anywhere.&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="nx"&gt;yq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yq-counter&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;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;button yq-on:click="inc"&amp;gt;{{ count }}&amp;lt;/button&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button { font-size: 18px; padding: 8px 18px; }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="na"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&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="nx"&gt;state&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;1&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;Then, in plain HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;yq-counter&amp;gt;&amp;lt;/yq-counter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tag mounts itself, renders, updates and cleans up. The template is HTML, the style is CSS, the script is JS — nothing custom to learn. No JSX, no virtual DOM, no compiler, and whoever drops the component on a page doesn't need a build step either.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;~12.6 kB&lt;/strong&gt; core bundle, gzipped&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;0&lt;/strong&gt; runtime dependencies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;298 / 298&lt;/strong&gt; tests passing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apache-2.0&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why another component framework?
&lt;/h2&gt;

&lt;p&gt;Fair question, there are plenty.&lt;/p&gt;

&lt;p&gt;But a lot of pages don't need a framework, they need one widget. If you pull React into a landing page for a single dropdown, you also bring its compiler, its runtime and a dependency tree. If you stay native with Web Components instead, you end up writing the templating, style isolation, reactivity and lifecycle wiring yourself, every time.&lt;/p&gt;

&lt;p&gt;yq-sanyi is for that middle ground. It removes the boilerplate, and the result is still a plain custom element — so it works in any page, any template engine, any CMS, next to whatever else is already on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/YQteam-hq/yq-sanyi.git
&lt;span class="nb"&gt;cd &lt;/span&gt;yq-sanyi
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then write a plain HTML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;yq-sanyi quick start&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;yq-counter&amp;gt;&amp;lt;/yq-counter&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./packages/core/dist/core.global.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;yq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yq-counter&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;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;button yq-on:click="inc"&amp;gt;{{ count }}&amp;lt;/button&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &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="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="na"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;state&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="nx"&gt;state&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;1&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Custom element rules still apply: the name starts lowercase and contains a hyphen, so &lt;code&gt;&amp;lt;yq-counter&amp;gt;&lt;/code&gt; works and &lt;code&gt;&amp;lt;counter&amp;gt;&lt;/code&gt; doesn't. &lt;code&gt;define&lt;/code&gt; throws on an invalid name instead of letting it fail quietly in the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Template, behavior and style, in one place
&lt;/h2&gt;

&lt;p&gt;Most setups keep structure, logic and style in three separate places. yq-sanyi keeps them in one definition that shares a scope, a reactive state and a lifecycle. That's where the name "trinity" comes from.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-trinity-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-trinity-en.png" alt="One definition, three parts, one scope: template, behavior and scoped style share one scope, one reactive state and one lifecycle" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The template layer is a set of declarative directives that covers the usual cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- list rendering with a stable key and a row index --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;yq-for=&lt;/span&gt;&lt;span class="s"&gt;"(task, index) in tasks"&lt;/span&gt; &lt;span class="na"&gt;yq-key=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {{ index }}. {{ task.title }}
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;yq-on:click=&lt;/span&gt;&lt;span class="s"&gt;"toggle"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;toggle&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- new in 0.4.0: nested yq-for at any depth, including on component tags --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;yq-for=&lt;/span&gt;&lt;span class="s"&gt;"group in groups"&lt;/span&gt; &lt;span class="na"&gt;yq-key=&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;{{ group.name }}&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;yq-row&lt;/span&gt; &lt;span class="na"&gt;yq-for=&lt;/span&gt;&lt;span class="s"&gt;"row in group.items"&lt;/span&gt; &lt;span class="na"&gt;yq-key=&lt;/span&gt;&lt;span class="s"&gt;"tag"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/yq-row&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Conditionals are &lt;code&gt;yq-if&lt;/code&gt; / &lt;code&gt;yq-else-if&lt;/code&gt; / &lt;code&gt;yq-else&lt;/code&gt; / &lt;code&gt;yq-show&lt;/code&gt;; two-way binding is &lt;code&gt;yq-model&lt;/code&gt;, with &lt;code&gt;.trim&lt;/code&gt; / &lt;code&gt;.number&lt;/code&gt; / &lt;code&gt;.lazy&lt;/code&gt;; events are written &lt;code&gt;yq-on:click&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Accessibility got its own pass in 0.4.0. When an &lt;code&gt;aria-*&lt;/code&gt; binding resolves to an empty value, the attribute isn't rendered at all, so you don't get a dangling &lt;code&gt;aria-label=""&lt;/code&gt; sitting in the DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
       &lt;span class="na"&gt;yq-model.trim=&lt;/span&gt;&lt;span class="s"&gt;"email"&lt;/span&gt;
       &lt;span class="na"&gt;yq-on:focus=&lt;/span&gt;&lt;span class="s"&gt;"markFocus"&lt;/span&gt;
       &lt;span class="na"&gt;yq-on:blur=&lt;/span&gt;&lt;span class="s"&gt;"validate"&lt;/span&gt;
       &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"{{ labelFor(name) }}"&lt;/span&gt;
       &lt;span class="na"&gt;aria-invalid=&lt;/span&gt;&lt;span class="s"&gt;"{{ error ? 'true' : '' }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Style isolation uses scope rewriting by default and doesn't require Shadow DOM. That's on purpose — global theming still reaches the component, and outside CSS isn't fully sealed off. When you do want a hard boundary, pass &lt;code&gt;useShadowDOM&lt;/code&gt; to &lt;code&gt;createScopedElement&lt;/code&gt;. A component's style is injected once and shared by every instance.&lt;/p&gt;

&lt;p&gt;State comes back from &lt;code&gt;script&lt;/code&gt;. Handlers receive the reactive state, and writing to it re-renders. Several writes in the same synchronous task are batched into one flush.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's new in v0.4.0
&lt;/h2&gt;

&lt;p&gt;Five merged batches, four areas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Nested &lt;code&gt;yq-for&lt;/code&gt; at any depth, with &lt;code&gt;yq-key&lt;/code&gt; and the row index still working&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;aria-*&lt;/code&gt; bindings, plus &lt;code&gt;yq-on:keydown&lt;/code&gt; / &lt;code&gt;focus&lt;/code&gt; / &lt;code&gt;blur&lt;/code&gt; / &lt;code&gt;paste&lt;/code&gt; / &lt;code&gt;wheel&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;defineAlias(displayName, realName)&lt;/code&gt; for hyphenated display names&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reusable static fragments via &lt;code&gt;&amp;lt;template id="x"&amp;gt;&lt;/code&gt; and &lt;code&gt;yq.fragment(id, html)&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;onRecover&lt;/code&gt; plus &lt;code&gt;yq.onError(fn)&lt;/code&gt; for structured failure handling&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;SSR and hydration&lt;/strong&gt;&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="nx"&gt;yq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseTemplateDSD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// recognize &amp;lt;template shadowrootmode&amp;gt; from the server&lt;/span&gt;
&lt;span class="nx"&gt;yq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hydrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// adopt server-rendered DOM in place, no rebuild&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Node renderer entry is still a skeleton, and &lt;code&gt;linkedom&lt;/code&gt; is an optional peer — that's what keeps the core dependency-free.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-ssr-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-ssr-en.png" alt="SSR -&gt; Declarative Shadow DOM -&gt; hydrate in place: server-rendered markup is adopted, not rebuilt" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reactive primitives&lt;/strong&gt;&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effectPre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effectScope&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="s1"&gt;./core.mjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;signal&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;off&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;// 0&lt;/span&gt;
&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// notifies subscribers&lt;/span&gt;
&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// 5 — reads without tracking&lt;/span&gt;
&lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// unsubscribe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also &lt;code&gt;effectPre&lt;/code&gt; (runs synchronously, returns a stop function) and &lt;code&gt;effectScope&lt;/code&gt; (disposal in a group, with an idempotent &lt;code&gt;stop()&lt;/code&gt;). &lt;code&gt;derived&lt;/code&gt; now works out on its own whether a dependency is a &lt;code&gt;signal&lt;/code&gt; or a &lt;code&gt;state&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ecosystem&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DevTools extension for Chrome and Firefox (manifest v3)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;React / Vue type wrappers (&lt;code&gt;.d.ts&lt;/code&gt; only, ≤ 1 kB)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A live playground in &lt;code&gt;examples/playground.html&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;New examples: SSR hydration, accessible form, nested lists, strict-CSP&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance is a CI gate
&lt;/h2&gt;

&lt;p&gt;Four performance budgets run in CI, and missing one fails the build, so a rendering regression can't land:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Budget&lt;/th&gt;
&lt;th&gt;Measured (p50)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;first-interactive&lt;/code&gt; — 3000-row cold boot to a click that reaches the DOM&lt;/td&gt;
&lt;td&gt;≤ 1000 ms&lt;/td&gt;
&lt;td&gt;~127 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;update-latency&lt;/code&gt; — state write replacing all 3000 rows until the DOM shows it&lt;/td&gt;
&lt;td&gt;≤ 200 ms&lt;/td&gt;
&lt;td&gt;~21 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;scroll-fps&lt;/code&gt; — 2000 rows, 200-row window, 4 rows per frame&lt;/td&gt;
&lt;td&gt;≥ 55 fps&lt;/td&gt;
&lt;td&gt;~880 fps (derived from p50 frame cost)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;scroll-frame-ops&lt;/code&gt; — DOM mutations to advance the window one frame&lt;/td&gt;
&lt;td&gt;≤ 900 ops/frame&lt;/td&gt;
&lt;td&gt;705 ops/frame&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last one is my favorite. It counts DOM operations instead of timing them. One scroll frame always costs exactly 705 mutations, so a fast laptop and a loaded CI runner report the same number, and that catches regressions timing alone can't see.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-perf-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-perf-en.png" alt="Four CI performance budgets, all green: green bar is the measured p50, dashed line is the budget" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, the static skeleton is cloned once and updates write only the bound slots. No subtree rebuilds, no virtual-DOM diffing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-render-en.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/.%2Fimages%2Ffig-render-en.png" alt="Clone the skeleton once, patch only the bound slots: updates write changed slots, with no subtree rebuild and no virtual-DOM diff" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSR is Declarative-Shadow-DOM based and opt-in.&lt;/strong&gt; The Node renderer is a skeleton, and browsers without DSD support fall back to client-side rendering.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The bundle is over its target.&lt;/strong&gt; The 0.4.0 goal was ≤ 11 kB gzipped for the core; it landed at 12.58 kB (&lt;code&gt;core.mjs&lt;/code&gt;) and 12.78 kB (&lt;code&gt;core.global.js&lt;/code&gt;), about 1.6 kB over. Every milestone added net-positive source bytes, and esbuild already runs &lt;code&gt;--minify&lt;/code&gt;, so squeezing harder yields less than 100 bytes. Getting under the target needs real feature cuts, tracked for v0.4.1. This release raises the CI size ceiling from 12 kB to 13 kB so the gate matches the size that actually shipped, instead of leaving a red check forever.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No CLI, and no non-browser target.&lt;/strong&gt; The core is browser-first on purpose.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather write these down than dress them up. Picking a component layer is a trade-off, and the limits are part of the decision.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Repository: &lt;a href="https://github.com/YQteam-hq/yq-sanyi" rel="noopener noreferrer"&gt;https://github.com/YQteam-hq/yq-sanyi&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;v0.4.0 release assets (&lt;code&gt;yq-sanyi-0.4.0.zip&lt;/code&gt; / &lt;code&gt;.tar.gz&lt;/code&gt;): &lt;a href="https://github.com/YQteam-hq/yq-sanyi/releases/tag/v0.4.0" rel="noopener noreferrer"&gt;https://github.com/YQteam-hq/yq-sanyi/releases/tag/v0.4.0&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tutorials: &lt;code&gt;docs/tutorial.md&lt;/code&gt; (English), &lt;code&gt;docs/i18n/zh-CN/tutorial.md&lt;/code&gt; (Chinese)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runnable examples: &lt;code&gt;examples/&lt;/code&gt; — playground, SSR hydration, a11y form, nested lists, CSP&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it on a real page, I'd like to hear where it breaks. Issues and PRs are welcome.&lt;/p&gt;

</description>
      <category>webcomponents</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Building multi-user + security on shared hosting: filesystem as the only state layer</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:33:22 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/building-multi-user-security-on-shared-hosting-filesystem-as-the-only-state-layer-449f</link>
      <guid>https://dev.to/yqteamdyq/building-multi-user-security-on-shared-hosting-filesystem-as-the-only-state-layer-449f</guid>
      <description>&lt;p&gt;The self-hosted utility space shares a quiet truth: once a tool grows past the "one admin logs in" stage, the real work is not adding pages — it's deciding how to let a few people cooperate, who gets to approve dangerous operations, and how to persist all that state when you can't bank on a daemon, a database, or Redis. I recently read through the source of an open-source panel built for PHP shared hosting, and it answered all three with little more than &lt;code&gt;.json&lt;/code&gt; files and file locks.&lt;/p&gt;

&lt;p&gt;This is not a feature review. It's a look at the implementation trade-offs I found, and a few places where I genuinely don't know the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default-deny is where multi-user gets done right
&lt;/h2&gt;

&lt;p&gt;The permission model is two layers: a role rank (viewer &amp;lt; operator &amp;lt; admin) plus an action whitelist. Ranking is just integer comparison — boring. The interesting part is the companion default policy: &lt;strong&gt;any endpoint not explicitly listed in the ACL map is admin-only&lt;/strong&gt;. A lot of old panels default to "if it's not written, everyone can call it" — one unguarded endpoint and you leak. Here the default is rejection, and you opt in. That direction matters more than the fancy parts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A path allowlist that's string-prefix matching raises flags
&lt;/h2&gt;

&lt;p&gt;To limit read-only users to a subtree, the check looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;rtrim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's raw string-prefix comparison. It does not resolve &lt;code&gt;..&lt;/code&gt;, and it does not resolve symlinks. If an allowed directory contains a symlink pointing outside the allowed tree, can the read-only user traverse it? That depends on whether the file layer above normalizes via &lt;code&gt;realpath&lt;/code&gt;. I stopped at the ACL layer and didn't chase the whole chain, so it stays an open question. Even a one-line comment ("the layer above normalizes paths") would save everyone the audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Double approval is, in effect, a re-dispatch with the gate bypassed
&lt;/h2&gt;

&lt;p&gt;Sensitive operations (DB import, bulk purge, app uninstall) are intercepted and routed through a second-admin approval. Two decisions here are right: a single-admin deployment gets &lt;code&gt;409&lt;/code&gt; outright instead of a "just let it through" fallback, and the requester cannot be the approver.&lt;/p&gt;

&lt;p&gt;The execution path is the part worth unpacking. When the approver clicks approve, the code rebuilds the router, overrides the request body with the stored payload, opens a capture buffer, sets a bypass flag, and &lt;strong&gt;synchronously re-dispatches the request inside the approver's own HTTP call&lt;/strong&gt;. Two consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A slow operation blocks the approver's request until it finishes. For a large import that's a UX and timeout question.&lt;/li&gt;
&lt;li&gt;The payload is a snapshot taken at submit time; by the time it runs, the underlying resource may have changed. So "approve" strictly means "approve the action as-of submit", not "apply to current state". Fine in most cases, worth remembering for auditing and error handling.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The rate limiter is honestly not a sliding window
&lt;/h2&gt;

&lt;p&gt;Rate limiting applies only to non-admin roles. State lives in &lt;code&gt;{userid}_{minute-bucket}.json&lt;/code&gt;, and the check sums the &lt;strong&gt;current bucket plus the previous one&lt;/strong&gt; to approximate "roughly two minutes":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$bucket&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$cur&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;count&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 not a sliding window; accuracy depends on where requests land relative to minute boundaries, and it skews at the seams. Fine as coarse shielding, but don't describe it as precise. One thing that caught my eye: reads and writes each count as 1 (no write weighting), while the read-only role's write quota is one request per minute. That is aggressive and likely to trip legitimate users — worth confirming it's intentional.&lt;/p&gt;

&lt;h2&gt;
  
  
  An in-process WAF lives and dies by its regexes
&lt;/h2&gt;

&lt;p&gt;The WAF runs on every PHP request, scanning query, body and URI with regex. Three implications worth separating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It blocks inside the PHP process. By the time it fires, PHP has already handled the request; it just stops it with &lt;code&gt;exit()&lt;/code&gt; before business code runs. This is an application-layer filter, not an edge firewall.&lt;/li&gt;
&lt;li&gt;The rules are keyword regexes, not semantic analysis. The command-injection rule contains &lt;code&gt;\b(?:ls|dir|cat|grep|find|exec|system|...)\b&lt;/code&gt;, so any plaintext containing "cat" or "find" trips it — search results, documents, an ordinary English sentence. And URL encoding or comment obfuscation sidesteps plain-text regex. It's a coarse sieve.&lt;/li&gt;
&lt;li&gt;It ships a per-IP 1000 req/hour all-up limit via a locked JSON file — a pragmatic last line of defense on shared hosting.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For shared hosting, the survival metric for this kind of WAF is the false-positive rate. Killing legitimate business costs more than the attacks it stops. "Rules can be turned off and scoped down" beats "the bigger the rule surface the better".&lt;/p&gt;

&lt;h2&gt;
  
  
  A convention everyone should copy: say plainly when a capability is missing
&lt;/h2&gt;

&lt;p&gt;When the host lacks a capability, the API returns &lt;code&gt;501&lt;/code&gt; with a machine-readable code (think &lt;code&gt;composer_unavailable&lt;/code&gt;, &lt;code&gt;fpm_not_applicable&lt;/code&gt;). Shared hosts routinely miss extensions or disable functions, and most panels just throw a generic 500 that sends you guessing. Separating "this host can't do it" from "the software is broken" is more valuable than a dozen more pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open questions I'm keeping
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Does the file layer above the path allowlist actually normalize &lt;code&gt;realpath&lt;/code&gt; and symlinks?&lt;/li&gt;
&lt;li&gt;With snapshot-replay approvals, when the resource changed after submit — replay anyway, or error and ask to resubmit?&lt;/li&gt;
&lt;li&gt;All state in JSON with file locks: at what concurrency and volume does that break, and where's the line where SQLite becomes worth it?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source I read is an open PHP panel; each module lives as one file under its &lt;code&gt;backend/&lt;/code&gt; directory, easy to browse: &lt;a href="https://github.com/YQteam-dyq/Go.js-Lite/" rel="noopener noreferrer"&gt;https://github.com/YQteam-dyq/Go.js-Lite/&lt;/a&gt;. If you've measured any of the above, I'd like to hear it.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>php</category>
      <category>security</category>
    </item>
    <item>
      <title>Why we wrote our own 8.5 kB web component runtime instead of pulling in a framework</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Mon, 07 Sep 2026 12:23:55 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/why-we-wrote-our-own-85-kb-web-component-runtime-instead-of-pulling-in-a-framework-6jh</link>
      <guid>https://dev.to/yqteamdyq/why-we-wrote-our-own-85-kb-web-component-runtime-instead-of-pulling-in-a-framework-6jh</guid>
      <description>&lt;p&gt;Hi, we're YQteam 👋 We just open-sourced &lt;a href="https://github.com/YQteam-dyq/yq-sanyi" rel="noopener noreferrer"&gt;yq-sanyi&lt;/a&gt; (v0.2.0, our initial release): &lt;strong&gt;define a component as a native HTML tag, then drop&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;&amp;lt;yq-counter&amp;gt;&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;into a plain page — zero dependencies, zero build.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first question people ask us is: there are already so many component solutions — why build your own?&lt;/p&gt;

&lt;p&gt;This post is not about the API. It's about the decision process behind the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  A scenario we kept hitting
&lt;/h2&gt;

&lt;p&gt;The need is simple: a reusable counter, a panel, a todo list — instead of copy-pasting the same HTML plus interaction logic into every page.&lt;/p&gt;

&lt;p&gt;But in practice you usually have only two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adopt a framework.&lt;/strong&gt; For a "lightweight reuse" need, you pay for a whole runtime plus a build toolchain. For existing static pages, multi-page sites, or server-rendered template pages, that feels like moving house just to buy a shelf.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hand-rolled copy-paste.&lt;/strong&gt; Great the first time, painful from the second: boilerplate, styles bleeding into each other, event listeners nobody remembers to clean up.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We wanted a third way: &lt;strong&gt;as light as copy-paste, with the reuse experience of a framework.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The three paths we walked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Native Web Components: the right direction, but parts, not a product
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;customElements&lt;/code&gt; standardized "UI as an element" — that's genuinely great. But to actually use a component comfortably you still have to solve a long list of problems yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;How do attributes and internal state stay in sync?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How should the event system be designed?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Where does reactive state come from?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What about style isolation? If you reach for Shadow DOM, form-element piercing, CSS-variable theming, and injecting external styles all become design problems of their own;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Who cleans up listeners and side effects on unmount?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The standard doesn't build those wheels for you. Using it raw means boilerplate; filling the gaps means writing a framework anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  A framework runtime: the cost doesn't match the benefit
&lt;/h3&gt;

&lt;p&gt;Our actual need was "a few reusable interactive widgets", not page-wide state management, routing, and an ecosystem. Bringing in the latter for the former is like opening a supermarket to buy a bottle of water.&lt;/p&gt;

&lt;p&gt;This is especially true when the target is &lt;strong&gt;plain HTML / multi-page / static pages, or a page embedded inside someone else's system&lt;/strong&gt; — a framework runtime plus build output can easily weigh more than the components themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  A minimal self-built loop: do just one thing — components
&lt;/h3&gt;

&lt;p&gt;So we implemented it ourselves, shrinking the scope to the smallest coherent unit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Let template, behavior and scoped style be one definition, one native tag, and one shared lifecycle.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The name "yq-sanyi" ("trinity") comes exactly from that idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules we set for ourselves
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Web-standard APIs only.&lt;/strong&gt; No JSX, no virtual DOM, no compiler, no framework runtime;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single source of truth.&lt;/strong&gt; Template, behavior and style live in one unit — easy to read, reuse and audit;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mutate state, the view updates itself.&lt;/strong&gt; Tags auto-mount, auto-update and auto-cleanup;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Style isolation without mandatory Shadow DOM.&lt;/strong&gt; Selector-scoping by default, CSS-variable theming, opt-in Shadow DOM when you need strong encapsulation;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failure isolation.&lt;/strong&gt; A broken component renders an error placeholder plus a structured warning while the rest of the page keeps working;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Size is a budget, not a souvenir.&lt;/strong&gt; The core is ~8.5 kB gzipped, and the repo ships dependency-graph and bundle-size gates plus benchmarks so it can't quietly bloat.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it looks like now
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;yq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yq-counter&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;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;button yq-on:click="inc"&amp;gt;count {{ count }}&amp;lt;/button&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button { font-size: 18px; padding: 8px 18px; }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &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="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&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="na"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&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="nx"&gt;state&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;1&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;Define it once. After that, the usage site is a single line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;yq-counter&amp;gt;&amp;lt;/yq-counter&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For template syntax, state, lists and scoped styles, the &lt;a href="https://github.com/YQteam-dyq/yq-sanyi" rel="noopener noreferrer"&gt;README&lt;/a&gt; and the &lt;a href="https://github.com/YQteam-dyq/yq-sanyi/blob/main/docs/tutorial.md" rel="noopener noreferrer"&gt;English tutorial&lt;/a&gt; cover everything — no need to repeat it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  We're not telling anyone to abandon their framework
&lt;/h2&gt;

&lt;p&gt;Different constraints call for different choices. If your project is deeply embedded in a framework ecosystem, needs SSR, or depends on a full CLI-driven pipeline — a framework is right for you, and we explicitly list those cases as "deliberately not supported" in our docs.&lt;/p&gt;

&lt;p&gt;But if you're on the &lt;strong&gt;"I just want to reuse a piece of UI, lightly"&lt;/strong&gt; side — no build, plain HTML pages, curious about how components actually work under the hood — come say hi at the repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Repo: &lt;a href="https://github.com/YQteam-dyq/yq-sanyi" rel="noopener noreferrer"&gt;https://github.com/YQteam-dyq/yq-sanyi&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bilingual tutorials and runnable examples live in the repo&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;v0.2.0 is our initial release and we will keep iterating&lt;/strong&gt;: fixing known limitations first (event binding inside &lt;code&gt;yq-for&lt;/code&gt; rows), evolving the template layer and devtools, and keeping the English/Chinese docs in sync. Stars, issues, and pull requests are all welcome — and if the project saves you time, you can &lt;a href="https://afdian.com/a/yqteam?utm_source=copylink&amp;amp;utm_medium=link" rel="noopener noreferrer"&gt;support us on Afdian&lt;/a&gt; to help keep it free, open and zero-dependency.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>webcomponents</category>
    </item>
    <item>
      <title>Why a New Compiler Splits Into AST, HIR, and TIR (and Caches by Content)</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Sat, 05 Sep 2026 14:03:46 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/why-a-new-compiler-splits-into-ast-hir-and-tir-and-caches-by-content-ki8</link>
      <guid>https://dev.to/yqteamdyq/why-a-new-compiler-splits-into-ast-hir-and-tir-and-caches-by-content-ki8</guid>
      <description>&lt;p&gt;Compiler beginners easily imagine compilation as a straight line: read source, parse, generate machine code. In real engineering, that line is split into several intermediate representations (IRs), where each layer does one set of jobs and layers are joined by lowering. Why go through all this trouble? Because a single representation always hits a conflict between expressiveness and information at some stage: too close to source and it is hard to optimize, too close to machine code and it is hard to analyze. This post uses YQ, an unreleased LLVM-based systems language we are developing, to explain what the AST, HIR, and TIR layers each do, why their order cannot be shuffled, and how a content-addressed cache keeps a multi-layer pipeline from becoming too slow to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three kinds of questions one compilation answers
&lt;/h2&gt;

&lt;p&gt;Splitting "turn YQ source into an executable" apart, a compiler is really answering three different kinds of questions in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What does this text mean&lt;/strong&gt; (syntax layer): what does the tree of &lt;code&gt;a + b * c&lt;/code&gt; look like?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are these names and types right&lt;/strong&gt; (semantic layer): is &lt;code&gt;b&lt;/code&gt; an &lt;code&gt;f32&lt;/code&gt; or a user-defined type? Does &lt;code&gt;+&lt;/code&gt; have a matching implementation?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How does this logic land on the machine&lt;/strong&gt; (code generation layer): is addition an &lt;code&gt;add&lt;/code&gt; instruction or a function call? How is a struct laid out in memory?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If one AST alone ran the whole way, the first kind of question would be easy to answer and the third would be very hard, because the AST is full of syntactic sugar (&lt;code&gt;if&lt;/code&gt;, pattern matching, operators, indentation blocks) and the optimizer would have to guess semantics every time. The industry-standard approach is therefore to let representations "desugar" layer by layer: the AST keeps the full syntactic shape of the source, and the further down the IR you go, the closer you get to a plain instruction-level form, until you reach LLVM IR, a representation that is optimizable and can generate code for many backends.&lt;/p&gt;

&lt;p&gt;YQ's pipeline is designed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;YQ source
  |  lexing/parsing (Unicode identifiers, indentation-aware, error recovery)
  v
Token -&amp;gt; AST (carries Spans: every node remembers its start/end location in source)
  |  semantic analysis: scope resolution, desugaring
  v
HIR (high-level IR: explicit control flow and calls, syntactic sugar flattened)
  |  type checking (HM inference + trait solving) + ownership/borrow checking
  v
TIR (typed IR: every node carries a concrete type)
  |  generic specialization (monomorphization) + lowering to LLVM IR
  v
LLVM IR -&amp;gt; object files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  AST: faithfully preserving syntax, Span is its soul
&lt;/h2&gt;

&lt;p&gt;The AST's job is not to be "convenient", but to be "complete". The parser (ours is a hand-written recursive descent one) turns source into a tree where every node hangs on a Span, the start/end line and column range of that node in the source file. A Span looks like a tiny structure, but it is the foundation of all diagnostics quality downstream:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;when types mismatch, the error can point precisely at the offending subexpression instead of the whole function;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;editor features like hover, go-to-definition, and completion are, at bottom, "map a cursor position onto an AST node";&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;if an error message wants to show a source snippet with terminal coloring, the only reliable source of that is the correspondence between Spans and source text.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also why even adding one field at the AST stage deserves caution: its structure flows through HIR and TIR and affects diagnostics and tooling all the way down. Many new language projects rush with a minimal AST to get something running, and later find that retrofitting the diagnostics experience means almost rewriting the front end. That bill is not worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  HIR: flattening "code written by humans" into "code a compiler can handle"
&lt;/h2&gt;

&lt;p&gt;HIR exists to remove syntactic sugar and implicit behavior. Code written by humans is full of conveniences: &lt;code&gt;if&lt;/code&gt; is an expression, operators call trait methods, pattern matching hides a chain of branch decisions, and indentation blocks must become explicit scopes. These are friendly to readers, but not to analyzers. The HIR stage does a few typical things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;scope and name resolution&lt;/strong&gt;: resolve names like &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;foo&lt;/code&gt; to definite definitions, producing binding relations for every symbol, preparing for type checking;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;explicitness&lt;/strong&gt;: expand syntactic sugar into plainer structures so later stages only face a few node shapes;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;keeping what type checking needs&lt;/strong&gt;: HIR nodes still have no types, but the call structure they record must be clean enough for the type checker to walk smoothly.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common design question is whether HIR should keep Spans. The answer is almost always yes. Diagnostics at ever later stages need to point back at source, and since HIR has already flattened syntactic sugar, dropping position information here means errors would point at "desugared intermediate nodes" that users cannot understand at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  TIR: the "final draft" after type and ownership checking
&lt;/h2&gt;

&lt;p&gt;Once type checking (HM-style inference plus trait constraint solving) and ownership checking finish, the compiler has enough information to pin every expression to a concrete type. That representation is TIR. Compared with HIR, the key increment of TIR is that every node carries a definite type, which makes it safe to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;generic specialization&lt;/strong&gt;: &lt;code&gt;add 1 2&lt;/code&gt; and &lt;code&gt;add 1.5 2.5&lt;/code&gt; are the same function node in HIR; in TIR the instantiation information is clear, so a dedicated copy can be cloned for &lt;code&gt;(i32, i32)&lt;/code&gt; and for &lt;code&gt;(f32, f32)&lt;/code&gt; and handed to LLVM for optimization;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;memory layout decisions&lt;/strong&gt;: struct field offsets and enum discriminant layouts can only really be computed once types are known;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ownership-related move/borrow decisions&lt;/strong&gt;: where a value moves and where it drops needs type information combined with region inference.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doing specialization at the TIR level instead of the LLVM level is so that "clone code by type" happens on our own IR. On our own IR we can freely copy, rename, and instrument nodes, while the LLVM layer is better at scalar optimization and instruction selection. The two jobs do not overlap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why hook up to LLVM instead of writing a whole back end
&lt;/h2&gt;

&lt;p&gt;Everything up to TIR is language-specific. The work after that, instruction selection, register allocation, optimization, and object file generation, is highly generic and an enormous amount of engineering. Calling into LLVM is what most new languages choose, and YQ does too: at the code generation layer, TIR is lowered to LLVM IR through LLVM's C API bindings, and LLVM produces object files. The benefit is getting an industrial-grade optimizer and multi-backend support for free (x86_64, WASM, and even GPU/TPU directions all build on the LLVM ecosystem). The cost is that you have to describe your language in LLVM's world view: you are responsible for struct layout decisions, calling conventions, and translating YQ's error handling model into control flow that LLVM can express.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost of a multi-layer pipeline: slowness, and how a cache covers it
&lt;/h2&gt;

&lt;p&gt;The most direct cost of multiple IR layers is slower compilation: every layer walks the whole tree once, and every lowering allocates again. Early new-language projects usually do not care, but once the compiler starts compiling itself (bootstrapping) and the test suite grows into the hundreds, slowness becomes a development-efficiency problem. Our approach is a content-addressed compilation cache: cache intermediate artifacts per file rather than per whole project.&lt;/p&gt;

&lt;p&gt;In the compile cache directory, each compilation unit leaves a group of files 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;1423ba0b-e3b0c442.meta     # metadata
1423ba0b-e3b0c442.bin      # intermediate artifact
1423ba0b-e3b0c442.o        # object file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.meta&lt;/code&gt; content is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;v&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;source_hash&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;SHA256 of the source&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;flags_hash&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;SHA256 of the compile options&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;deps&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;list of source files this unit depends on&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;corresponding .o file name&amp;gt;&lt;/span&gt;
&lt;span class="py"&gt;bin&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;corresponding .bin file name&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The keyword of this design is "content addressing": the cache key is not a file name or timestamp but a hash of the input content. Change one byte of source and &lt;code&gt;source_hash&lt;/code&gt; changes, so that unit is automatically invalidated and rebuilt; units that did not change are reused directly from &lt;code&gt;.o&lt;/code&gt;/&lt;code&gt;.bin&lt;/code&gt;. The &lt;code&gt;deps&lt;/code&gt; field records dependency relations, which makes "only recompile affected modules" possible. That is the skeleton of incremental compilation, and it lays the groundwork for toolchain goals such as "a full rebuild after changing one line stays under 500 ms". Note that what is hashed is the recomputable input, not the artifact path; otherwise the cache would silently hand you stale results.&lt;/p&gt;

&lt;p&gt;It is worth stressing: cache correctness matters far more than cache hit rate. Better to invalidate too much and rebuild than to let an old artifact slip into a new build. That is why compile options are folded into the hash as well (&lt;code&gt;flags_hash&lt;/code&gt;), ruling out the classic bug of "switching optimization levels yet eating the old cache".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Looking back over the pipeline: the AST is responsible for faithfully preserving syntax and carrying Spans, HIR for flattening syntactic sugar so it can be analyzed, TIR for finalizing after type and ownership checking and performing specialization, and LLVM for the last mile of optimization and code generation. Each layer boundary corresponds to a clear analysis phase in the compiler, and once the order is reversed you are forced to do work on a representation that it was never meant for. The content-addressed cache then pays off the "performance debt" of the multi-layer pipeline, keeping iteration speed usable after bootstrapping and test growth. For new language projects, planning this "layering plus content addressing" combination early is more valuable than jumping straight to a toy compiler, because once the front-end structure is fixed, later changes mean rewriting layer by layer.&lt;/strong&gt;&lt;br&gt;
**&lt;/p&gt;

</description>
      <category>programming</category>
      <category>compilers</category>
      <category>llvm</category>
    </item>
    <item>
      <title>I built a local memory layer for AI agents: no vector DB, one SQLite file</title>
      <dc:creator>YQteam</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:34:48 +0000</pubDate>
      <link>https://dev.to/yqteamdyq/i-built-a-local-memory-layer-for-ai-agents-no-vector-db-one-sqlite-file-27jf</link>
      <guid>https://dev.to/yqteamdyq/i-built-a-local-memory-layer-for-ai-agents-no-vector-db-one-sqlite-file-27jf</guid>
      <description>&lt;p&gt;If you build AI agents, you know the feeling: the model itself has no memory. Every conversation starts from zero. To make an agent remember user preferences, pick up where a task left off, or reuse knowledge across sessions, you have to bolt on a memory system yourself.&lt;/p&gt;

&lt;p&gt;I looked at the existing options, and none of them felt right for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Vector databases (Pinecone, Qdrant, Weaviate…) are powerful, but standing up a distributed service just for memory is a lot of weight for a small project or a solo dev.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cloud memory services (Mem0 and similar) keep your data on someone else's servers and bill per use.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rolling my own in-memory store loses everything on restart — and forget semantic search entirely.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I actually wanted was pretty boring: a local memory layer that runs from one file and one command, works out of the box, and keeps the data fully in my hands. I couldn't find one I liked, so I wrote it. That's how &lt;strong&gt;yq-nova-agent&lt;/strong&gt; started, open sourced on August 3rd: &lt;a href="https://github.com/YQteam-dyq/yq-nova-agent" rel="noopener noreferrer"&gt;github.com/YQteam-dyq/yq-nova-agent&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;In one sentence: a single-file SQLite memory and state layer for agents, built around three operations — &lt;strong&gt;remember&lt;/strong&gt;, &lt;strong&gt;recall&lt;/strong&gt;, &lt;strong&gt;forget&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Things an agent learns persist across conversations and survive restarts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can recall semantically relevant information from past sessions using natural language.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It tracks entities and their relationships, so you get lightweight graph reasoning.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stale or low-importance memories are cleaned up automatically, so the store doesn't grow forever.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No external service to deploy. The only runtime dependency is one SQLite file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions worth talking about
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Three operations, one mental model
&lt;/h3&gt;

&lt;p&gt;The API is deliberately small. HTTP, the Rust SDK, and the CLI all expose the same semantics, so there's no conceptual overhead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Store a fact with tags and an importance score&lt;/span&gt;
yq-nova remember &lt;span class="s2"&gt;"User is a Rust developer who prefers lightweight tools"&lt;/span&gt; &lt;span class="nt"&gt;--tag&lt;/span&gt; user-profile &lt;span class="nt"&gt;--importance&lt;/span&gt; 0.9

&lt;span class="c"&gt;# Recall with natural language&lt;/span&gt;
yq-nova recall &lt;span class="s2"&gt;"user's technical background"&lt;/span&gt; &lt;span class="nt"&gt;--top-k&lt;/span&gt; 5

&lt;span class="c"&gt;# Check what's in the store&lt;/span&gt;
yq-nova stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't want an HTTP server? The CLI works standalone. Building a Rust app? Pull in &lt;code&gt;yq-nova-core&lt;/code&gt; as a library and call it in-process.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid retrieval, not just vectors
&lt;/h3&gt;

&lt;p&gt;An early version that only did vector similarity missed too much: exact keyword matches and graph relationships between entities don't show up in pure semantic search. So recall fuses three signals with RRF (Reciprocal Rank Fusion):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Semantic search (embedding similarity)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keyword search (SQLite FTS5 full-text index)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Graph signals (entity-relation relatedness)&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Hybrid mode with graph enhancement&lt;/span&gt;
yq-nova recall &lt;span class="s2"&gt;"storage solutions related to SQLite"&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; hybrid &lt;span class="nt"&gt;--graph&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embeddings are pluggable: OpenAI-compatible endpoints by default, plus a built-in mock provider so you can develop and test fully offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Entities and relations: memory with context
&lt;/h3&gt;

&lt;p&gt;Isolated memory entries aren't enough — pieces of information relate to each other. The project keeps an entity-relation graph with recursive BFS traversal. Store "React is a UI library" and "Vue is a UI library", and a recall can walk the graph to find neighboring concepts. That's context pure vector search can't give you.&lt;/p&gt;

&lt;h3&gt;
  
  
  SQLite is underrated
&lt;/h3&gt;

&lt;p&gt;A lot of people write SQLite off as a toy, but with WAL mode, composite indexes, and FTS5 it's genuinely enough for a single-node memory layer — and the operational cost is close to zero. Persistence, transactions, and schema migrations are all solved problems, so I didn't have to reinvent any of them. This is also what makes the "zero external dependencies" promise hold up.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v0.2.0 added
&lt;/h2&gt;

&lt;p&gt;v0.2.0 shipped on August 6th, and it closed most of the gap between "works on my machine" and "usable in production":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Embedded SDK mode (&lt;code&gt;EmbeddedNova&lt;/code&gt;) — use it in-process without spinning up an HTTP server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local ONNX inference via FastEmbed, so you don't need an OpenAI API key to get vectors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;API token auth middleware on the HTTP server&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQLite vector index backed by sqlite-vec's HNSW&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Integration tests plus Criterion benchmarks (KNN, graph traversal, embedding)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker image and docker-compose for one-command startup&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A small Python client (&lt;code&gt;yq_nova&lt;/code&gt;)&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run the server in Docker, persisting data to /data&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 7999:7999 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; yq-nova-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;YQ_NOVA_EMBEDDING__DEFAULT_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mock &lt;span class="se"&gt;\&lt;/span&gt;
  yq-nova serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auth, OpenTelemetry tracing, and benchmarks are the "invisible" features — but they're exactly what you need before trusting something in production, so I prioritized them in v0.2.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;The project is new. It's been open source since August 3rd, and today it has zero stars and an issue tracker that only I talk to. I'm the only maintainer, and there are 18 commits so far. I'm not writing this to claim I built something impressive — I'm writing it because I hit a real problem (agent memory) that I think the "lightweight + local + single file" approach genuinely solves, and I want more people who feel the same pain to find it.&lt;/p&gt;

&lt;p&gt;If you're building agents and have opinions about memory, or you try it and think something is designed wrong, please open an issue. What I'd most like to know:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you handle agent memory today, and what hurts the most about it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your answers will directly shape what I build next — v0.3 is in active development, and I'd rather prioritize based on real scenarios than my own guesses.&lt;/p&gt;

&lt;p&gt;If the project is useful to you, or the direction sounds interesting, a star is the easiest way to help.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/YQteam-dyq/yq-nova-agent" rel="noopener noreferrer"&gt;github.com/YQteam-dyq/yq-nova-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm YQteam-dyq on GitHub — happy to chat about agent memory, Rust, or anything in between.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>sqlite</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
