<?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: Florian Rappl</title>
    <description>The latest articles on DEV Community by Florian Rappl (@florianrappl).</description>
    <link>https://dev.to/florianrappl</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%2F268296%2Feb6a1ad0-17d3-4302-9c44-a263a666b072.jpg</url>
      <title>DEV Community: Florian Rappl</title>
      <link>https://dev.to/florianrappl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/florianrappl"/>
    <language>en</language>
    <item>
      <title>My OSS Projects: Netpack</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Tue, 06 Oct 2026 08:06:29 +0000</pubDate>
      <link>https://dev.to/florianrappl/my-oss-projects-netpack-3274</link>
      <guid>https://dev.to/florianrappl/my-oss-projects-netpack-3274</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the fifth post in my &lt;strong&gt;"My Open-Source Projects"&lt;/strong&gt; series, where I go through some of the OSS projects I started or maintain and tell you the story behind them. So far: &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;AngleSharp&lt;/a&gt;, &lt;a href="https://dev.to/florianrappl/my-oss-projects-mages-31cl"&gt;MAGES&lt;/a&gt;, &lt;a href="https://dev.to/florianrappl/my-oss-projects-piral-2856"&gt;Piral&lt;/a&gt;, and &lt;a href="https://dev.to/florianrappl/my-oss-projects-electronnet-2cgl"&gt;Electron.NET&lt;/a&gt;. This time it's the youngest of the bunch: &lt;strong&gt;Netpack&lt;/strong&gt; - a project that started as an experiment, is not at 1.0 yet, and has a development story that looks very different from everything above.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is Netpack, Actually?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/FlorianRappl/netpack" rel="noopener noreferrer"&gt;&lt;strong&gt;Netpack&lt;/strong&gt;&lt;/a&gt; is a web bundler written in C#/.NET. You point it at an &lt;code&gt;index.html&lt;/code&gt; (like you would with Vite or Parcel) or directly at a &lt;code&gt;.js&lt;/code&gt; / &lt;code&gt;.ts&lt;/code&gt; file, and it follows everything that is referenced from there, bundles it, optimizes it, and writes the result to disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; netpack
npx netpack bundle src/index.html &lt;span class="nt"&gt;--minify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, I know. Another bundler. I can hear the sigh from here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs5rxyntsvz3we9z9eq46.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs5rxyntsvz3we9z9eq46.gif" alt="oh no, not another JavaScript build tool" width="220" height="146"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But there's a twist that makes this one worth a post: the CLI is a &lt;strong&gt;single native binary&lt;/strong&gt; (no .NET runtime to install, nothing to warm up), it ships with batteries included (TypeScript, JSX, CSS &amp;amp; CSS Modules, Sass / LESS / PostCSS including Tailwind, images, Vue, Astro, Svelte, Solid, ...), and - since it is &lt;em&gt;written in C#&lt;/em&gt; - it can also be used as a plain &lt;strong&gt;.NET library&lt;/strong&gt; or an &lt;strong&gt;MSBuild task&lt;/strong&gt;. That last part is where I see the real future of the project. More on that later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question
&lt;/h2&gt;

&lt;p&gt;It all started during my winter vacation with a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can web tooling written in C#/.NET compete with tooling written in Rust, Go, or Zig?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Look at the tools we use every day: esbuild is written in Go, rspack and SWC are written in Rust, Bun is written in Zig. These languages are a great fit for native tooling - and the results speak for themselves. Their developer ergonomics, on the other hand, are... let's say "acquired tastes". I'm a .NET guy. I find C# much more approachable than Rust and more capable than Go. So the question was: could .NET work here? Maybe only under certain constraints - but could it?&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://dev.to/florianrappl/netpack-5ej4"&gt;wrote it up back in January 2025&lt;/a&gt;, included a lot of caveats, and ended with a question to the community: viable idea or just trash? This post is, in a way, the long answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Fake a Bundler with a Regular Expression
&lt;/h2&gt;

&lt;p&gt;To find out, I started with the most simple approach imaginable - a &lt;em&gt;fake&lt;/em&gt; bundler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open files&lt;/li&gt;
&lt;li&gt;Read their content&lt;/li&gt;
&lt;li&gt;Use a regular expression to find things like &lt;code&gt;import&lt;/code&gt; statements&lt;/li&gt;
&lt;li&gt;Resolve the linked modules&lt;/li&gt;
&lt;li&gt;Look at &lt;code&gt;package.json&lt;/code&gt; files to figure out where a package's entry point lives&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(If you've read the &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;AngleSharp post&lt;/a&gt; you might feel a disturbance right now. Yes: I am aware that "parse code with a regex" is exactly the thing I spent a decade telling people not to do. This was an &lt;em&gt;experiment&lt;/em&gt;. Experiments are allowed to be dirty.)&lt;/p&gt;

&lt;p&gt;And quite quickly it became clear that plain .NET is, as expected, too slow for this. Not because C# is slow at reading files or walking graphs - the IO and the algorithms are perfectly fine. What kills it is the &lt;strong&gt;startup cost&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By default, .NET compiles your code to an intermediate language, and a &lt;strong&gt;JIT&lt;/strong&gt; (just-in-time) compiler turns that into machine code while the program runs. That needs the .NET runtime to be present, started, and then fed with your code every single time. For a long-running server, nobody cares. For a bundler that you start, let finish in a few hundred milliseconds, and then throw away - it's the whole game.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: To Beat Native, You Need Native
&lt;/h2&gt;

&lt;p&gt;The experiment wasn't over, though, because .NET has an answer for that: &lt;strong&gt;AoT&lt;/strong&gt; (ahead-of-time) compilation. Instead of shipping intermediate language and compiling at runtime, you compile everything to machine code beforehand. The runtime is fused into the binary, there is no JIT, and the program starts right away.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwmsvxd60gb33hi8mn6vx.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%2Fwmsvxd60gb33hi8mn6vx.png" alt="JIT vs. AoT: plain .NET starts the runtime, loads assemblies and JIT-compiles code before the actual work begins, while the AoT binary starts and goes straight to the actual work" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And... it worked. This proved that the IO performance is truly native and that C#/.NET is in the same ballpark as Rust, Zig, or Go for this kind of tool. (There is still a garbage collector in the room, so it is not &lt;em&gt;identical&lt;/em&gt; - but the part that kills small tools, the startup, was gone.)&lt;/p&gt;

&lt;p&gt;AoT does come with its own challenges. Some libraries cannot be used or need work to be AoT-compatible, and you lose some of the dynamic flexibility that .NET is known for - no runtime reflection, for instance. But for a tool like this, it's a deal I'm happy to take.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08pmihml7iduuwgvzz0y.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F08pmihml7iduuwgvzz0y.gif" alt="starts instantly, no warm-up" width="498" height="275"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Real Parsing, Borrowed Parts
&lt;/h2&gt;

&lt;p&gt;Armed with that insight, I replaced the regular expressions with actual code understanding. For the JavaScript side I used a third-party parsing library (&lt;a href="https://github.com/adams85/acornima/" rel="noopener noreferrer"&gt;Acornima&lt;/a&gt;). For HTML and CSS I used my own project, &lt;a href="https://github.com/AngleSharp/AngleSharp" rel="noopener noreferrer"&gt;AngleSharp&lt;/a&gt; (which, I'm happy to report, is still doing great - it gets a second career here).&lt;/p&gt;

&lt;p&gt;The result was a bundler fast enough to compete with the established tools, such as esbuild or rspack - at least on the benchmark projects I used back then. It could only handle plain JavaScript, though. No TypeScript, no source maps, no real configuration. I published the experiment, called it done, and moved on.&lt;/p&gt;

&lt;p&gt;For quite a while that's where I saw the project: &lt;em&gt;done&lt;/em&gt;. A successful experiment, a proof that the idea works, a repo with a README and some benchmark numbers. The missing features (TypeScript above all) would have required forking the parsing library, and I only wanted to do that "if there is enough buzz around the project".&lt;/p&gt;

&lt;p&gt;Plot twist: it wasn't buzz that got Netpack moving again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Enter the AI Pair Programmer
&lt;/h2&gt;

&lt;p&gt;With the advancements in AI-assisted software development, I took another look. And I realized that many of the missing things - full TypeScript support being the biggest one - could now be added &lt;em&gt;easily&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Here's what made the difference: the AI (mostly Claude) has sufficient understanding of the project to make modifications &lt;strong&gt;within the given architecture&lt;/strong&gt;. That's the key part. The original architecture of the bundler stayed intact. The AI doesn't redesign the thing - it adds missing features and takes care of important bug fixes, inside the structure that was already there.&lt;/p&gt;

&lt;p&gt;That's not luck, by the way, it's a bit of homework. The repository contains a &lt;a href="https://github.com/FlorianRappl/netpack/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/a&gt; that explains the architecture and the non-obvious conventions: how the graph is built, how output formats and platforms plug in, that everything has to stay &lt;strong&gt;AoT-safe&lt;/strong&gt; (no reflection, source-generated JSON), and even which names look like they need "tidying" but must never be touched because they hold the NuGet / AoT story together. There's an xUnit test suite with its own conventions, and the file explicitly tells the AI not to claim that tests pass when it couldn't run them. Good rules for humans, too.&lt;/p&gt;

&lt;p&gt;The results speak for themselves. The native toolchain today includes a &lt;strong&gt;hand-written JavaScript / TypeScript / JSX tokenizer, parser, printer, minifier, and tree-shaker&lt;/strong&gt; - no Acornima, Babel, or SWC involved. Compare the numbers: in January 2025 I was debating whether the project deserved a fork of someone else's parser. Today it has its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhz0z1jlh6w65c1shposz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhz0z1jlh6w65c1shposz.gif" alt="Speedrun!" width="600" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the pace went from "weekend experiment" to "release train":&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3tnnhp3j8l1nygbl7kcb.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%2F3tnnhp3j8l1nygbl7kcb.png" alt="Netpack timeline: the experiment announced on 9 Jan 2025, then - after about 18 months and a switch from a month-scale to a day-scale axis - fifteen releases from v0.1.0 on 10 July to v0.8.2 on 19 August 2026, with the .NET libraries arriving on NuGet on 24 July and a planned 1.0 at the end of 2026" width="800" height="1560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that the axis switches from months to days after the break - there's simply no other way to fit it on a screen. The first GitHub release (0.1.0) was on July 10th, 2026. By August 19th we were at 0.8.2. That's fifteen versions in under six weeks, including days with two or even three releases. My commit history now reads like a speedrun.&lt;/p&gt;

&lt;p&gt;Not everything on that timeline is mine, either. Since version 0.6.0, there are also contributions from the outside - most notably from &lt;a href="https://github.com/mehrabix" rel="noopener noreferrer"&gt;mehrabix&lt;/a&gt;, who contributed a whole range of things: the plugin hook system, improved watch mode and incremental builds, asset inlining, the &lt;code&gt;preview&lt;/code&gt; command, &lt;code&gt;.env&lt;/code&gt; support, better error messages with source snippets, CSS code splitting, split-chunks grouping, and more. A massive thank you!&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Whatever you throw at it, the shape of a build is always the same: Netpack parses the entry, follows what it references to build a module graph, groups that graph into chunks (pulling shared code into its own chunk), and renders each chunk to disk.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0wfx1lti2pcd3kcv7lvw.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%2F0wfx1lti2pcd3kcv7lvw.png" alt="Netpack build pipeline: an entry (HTML, JS or TS) is parsed, followed into a module graph, grouped into chunks, and emitted; all inside one AoT-compiled native binary, with an opt-in Node bridge for Sass, LESS, PostCSS, Svelte, Solid, and codegen, and native C# handling of TypeScript, JSX, CSS, Vue, Astro, and images" width="800" height="294"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few details worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most things are handled &lt;strong&gt;natively in C#&lt;/strong&gt;: TypeScript, JSX, CSS, CSS Modules, images (optimized with SkiaSharp in the CLI), JSON, HTML, and even the &lt;strong&gt;Vue&lt;/strong&gt; and &lt;strong&gt;Astro&lt;/strong&gt; compilers - no Node.js round trip required.&lt;/li&gt;
&lt;li&gt;A few things have a canonical compiler that lives in the JavaScript world: &lt;strong&gt;Sass, LESS, PostCSS (including Tailwind), Svelte, Solid, and &lt;code&gt;.codegen&lt;/code&gt; files&lt;/strong&gt;. For those, Netpack has an opt-in &lt;strong&gt;Node bridge&lt;/strong&gt; that talks to your locally installed packages. If you don't use them, you don't need Node.js for the build. (Yes, the .NET bundler sometimes calls Node. I see the irony too.)&lt;/li&gt;
&lt;li&gt;The dev server watches the file system, rebuilds on change, and hot-swaps modules where it can - including &lt;strong&gt;React Fast Refresh&lt;/strong&gt; if &lt;code&gt;react-refresh&lt;/code&gt; is installed. If it can't, it falls back to a full reload.&lt;/li&gt;
&lt;li&gt;Some things are a bit unusual, and I like them: if your HTML entry point contains an &lt;strong&gt;import map&lt;/strong&gt;, the entries in that map are automatically treated as externals. And if you mark dependencies as &lt;code&gt;--shared&lt;/code&gt;, Netpack builds them as their own chunks and wires them into an import map for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;You install it from npm. The wrapper package picks the right native binary for your platform (Windows x64 / ARM64, Linux x64 / ARM64, macOS ARM64):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; netpack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then there are three main commands:&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;# one-shot production build&lt;/span&gt;
npx netpack bundle src/index.html &lt;span class="nt"&gt;--minify&lt;/span&gt; &lt;span class="nt"&gt;--sourcemap&lt;/span&gt;

&lt;span class="c"&gt;# dev server with HMR (default port 1234)&lt;/span&gt;
npx netpack serve src/index.html

&lt;span class="c"&gt;# inspect what ended up in your bundle&lt;/span&gt;
npx netpack analyze src/index.html &lt;span class="nt"&gt;--interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need an HTML file; a script is a fine entry point, too: &lt;code&gt;npx netpack bundle src/main.tsx&lt;/code&gt;. Your &lt;code&gt;package.json&lt;/code&gt; scripts will look like they always do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"netpack serve src/index.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"netpack bundle src/index.html --minify --sourcemap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"analyze"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"netpack analyze src/index.html --interactive"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want React and friends as separate, cacheable chunks wired up through an import map? Mark them as shared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx netpack bundle src/index.html &lt;span class="nt"&gt;--shared&lt;/span&gt; react &lt;span class="nt"&gt;--shared&lt;/span&gt; react-dom
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;analyze&lt;/code&gt; command deserves a shout-out: besides the interactive graph explorer, it audits the dependencies that actually made it into your bundle against known vulnerabilities and gives suggestions for where your chunks could be split more efficiently. (Tree-shaken packages are not audited - only what ships.)&lt;/p&gt;

&lt;p&gt;For module-federation fans (hi, &lt;a href="https://dev.to/florianrappl/my-oss-projects-piral-2856"&gt;Piral&lt;/a&gt; people): a &lt;code&gt;federation.json&lt;/code&gt; entry point gives you &lt;strong&gt;Module Federation&lt;/strong&gt; and &lt;strong&gt;Native Federation&lt;/strong&gt; remotes. Netpack also supports ESM, CommonJS, UMD, and SystemJS output, plus web, Node, and Deno targets. Everything is covered in the &lt;a href="https://netpack.anglevisions.com/docs/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using It from .NET
&lt;/h2&gt;

&lt;p&gt;Here's the part where being written in C# pays off. Netpack is not just a CLI - there are three ways in:&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftrbae59jd8r28ay24dzc.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%2Ftrbae59jd8r28ay24dzc.png" alt="Netpack from .NET: the NetPack.Core engine at the center, used by the netpack npm CLI, the NetPack.Build MSBuild task, the Node.js API, and the parser as a library, with ASP.NET Core apps and Cake / Fallout build scripts as what the library makes possible" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The MSBuild task.&lt;/strong&gt; &lt;a href="https://www.nuget.org/packages/NetPack.Build" rel="noopener noreferrer"&gt;&lt;code&gt;NetPack.Build&lt;/code&gt;&lt;/a&gt; bundles your web entry point into &lt;code&gt;wwwroot&lt;/code&gt; as part of &lt;code&gt;dotnet build&lt;/code&gt; / &lt;code&gt;dotnet publish&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package NetPack.Build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;NetpackEntry&amp;gt;&lt;/span&gt;ClientApp/src/index.html&lt;span class="nt"&gt;&amp;lt;/NetpackEntry&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build
&lt;span class="c"&gt;# -&amp;gt; wwwroot/index.html, wwwroot/index.js, wwwroot/styles.css, ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it - no &lt;code&gt;npm run build&lt;/code&gt; step to remember, no extra pipeline stage. There are more MSBuild properties (&lt;code&gt;NetpackMinify&lt;/code&gt;, &lt;code&gt;NetpackSourceMaps&lt;/code&gt;, &lt;code&gt;NetpackFormat&lt;/code&gt;, &lt;code&gt;NetpackEntryNames&lt;/code&gt;, ...) and an item group for externals. It's built on the core library and ships a pure-managed image processor, so there's no native dependency to worry about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The library.&lt;/strong&gt; &lt;a href="https://www.nuget.org/packages/NetPack.Core" rel="noopener noreferrer"&gt;&lt;code&gt;NetPack.Core&lt;/code&gt;&lt;/a&gt; is the bundler engine as a managed library. Its only dependencies are AngleSharp and its CSS companion (see, I told you it's still doing great):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NetPack&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// in memory ...&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Bundler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BundleAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"src/index.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BundleOptions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Minify&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Platform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Web&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ModuleFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Esm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;indexHtml&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"index.html"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// ... or straight to a directory&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Bundler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteToDirectoryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"src/index.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BundleOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Minify&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SourceMaps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&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;3. The parser, as a library.&lt;/strong&gt; The TypeScript / JSX front-end is public, too - so you can parse, transform, and print without bundling anything. For instance, you can turn TypeScript into JavaScript from C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NetPack.Syntax&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NetPack.Syntax.Printer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParseModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"const x: number = 1;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"in.ts"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;js&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsPrinter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// -&amp;gt; "const x = 1;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does that matter? Because things that were awkward when your bundler lives in another ecosystem suddenly become straightforward. A bundler that is just a .NET library can be part of an ASP.NET Core web server, or of a build pipeline based on MSBuild, &lt;a href="https://cakebuild.net/" rel="noopener noreferrer"&gt;Cake&lt;/a&gt;, or &lt;a href="https://docs.fallout.build/" rel="noopener noreferrer"&gt;Fallout&lt;/a&gt; - no child process, no JSON handshake, no "please have Node.js installed on the build agent" in your onboarding doc. It can post-process and optimize the output of ASP.NET Core and Blazor applications from within the same process. Many optimizations and use cases become possible that were difficult before. (The two dashed boxes in the diagram are exactly that: things the library makes possible, which I'm still exploring, versus the solid ones that exist today.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F886ynwoa5uhdjleek2jy.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F886ynwoa5uhdjleek2jy.gif" alt="finally, they're speaking my language" width="374" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How Fast Is It, Really?
&lt;/h2&gt;

&lt;p&gt;Time for the honest part. In January 2025, the numbers looked fantastic: netpack finished the small project in 418 ms, esbuild needed 670 ms, rspack roughly 910 ms, and Vite 1.66 s. (That table is, in fact, still on the website. I should update that.) But the world didn't stand still. esbuild got faster, rspack got a major version, and Vite switched to a Rust-based bundler (Rolldown) with version 8.&lt;/p&gt;

&lt;p&gt;Here are the current numbers from the repository's &lt;a href="https://github.com/FlorianRappl/netpack/blob/main/performance.md" rel="noopener noreferrer"&gt;&lt;code&gt;performance.md&lt;/code&gt;&lt;/a&gt; - netpack 0.6.0 against the latest versions of the others, measured with hyperfine on the same test projects (the table was last run for 0.6.0 - the project has moved on to 0.8.2 since):&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffe20ct0dgec8sqmfpz69.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%2Ffe20ct0dgec8sqmfpz69.png" alt="Benchmark chart: netpack 0.6.0 is fastest on library builds at 141 ms, ties esbuild on the small project at 240 ms, beats esbuild on medium and large projects (637 and 595 ms vs. 819 and 895 ms), but rspack 2.1.5 and Vite 8.1.5 are faster on medium and large projects" width="799" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My reading of that chart:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Library builds&lt;/strong&gt;: netpack wins (141 ms vs. 150 ms for esbuild).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small project&lt;/strong&gt;: a dead heat with esbuild (240 ms vs. 238 ms), both ahead of rspack and Vite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium and large projects&lt;/strong&gt;: netpack comfortably beats esbuild, but &lt;strong&gt;rspack 2 and Vite 8 are faster&lt;/strong&gt; (around 450 ms vs. 595 ms on the large project).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So: &lt;em&gt;still in the same ballpark as the native tools&lt;/em&gt;, and the original thesis holds - but no, it doesn't "beat everyone" anymore. As always, take benchmarks (especially the ones the author runs himself) with a healthy dose of salt. And note that features like source maps and tree-shaking cost time, too - more features, more work to do per build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Native startup.&lt;/strong&gt; An AoT-compiled binary that starts instantly and behaves the same whether it bundles two files or a large app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-config and batteries-included.&lt;/strong&gt; Same entry-point convention as Vite or Parcel, with TypeScript, JSX, CSS Modules, Sass / LESS / PostCSS, images, Vue, Astro, Svelte, and Solid on board.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real toolchain, not a wrapper.&lt;/strong&gt; Hand-written tokenizer, parser, printer, minifier, and tree-shaker - all in readable C#. (Arguably more readable than the Rust equivalent. I said &lt;em&gt;arguably&lt;/em&gt;.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET-native.&lt;/strong&gt; Library, MSBuild task, and parser - all usable without leaving the .NET world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart defaults for modern web.&lt;/strong&gt; Import maps as externals, &lt;code&gt;--shared&lt;/code&gt; dependencies, Module and Native Federation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A genuinely useful analyzer&lt;/strong&gt;, with dependency audits and optimization suggestions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Struggles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's pre-1.0 and experimental.&lt;/strong&gt; The README still carries a warning that it's not production-ready, and it's still true: the chance that it works end-to-end for &lt;em&gt;your&lt;/em&gt; project today isn't as high as I'd like. Please test it on side projects first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ecosystem is the elephant in the room.&lt;/strong&gt; Vite is downloaded around 65 million times a week. Its plugin ecosystem, its documentation, and its integration with every framework under the sun can't be replicated - and I'm not going to pretend otherwise. Netpack has &lt;em&gt;hooks&lt;/em&gt; (inspired by rspack), but don't expect to drop in your existing Vite plugins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every framework is there.&lt;/strong&gt; Angular is not supported (yet). Svelte and Solid compile through the Node bridge; Vue and Astro are native, but with limitations documented in their respective pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not every platform is there.&lt;/strong&gt; There are binaries for Windows x64 / ARM64, Linux x64 / ARM64, and macOS ARM64. macOS x64 and Windows x86 are still missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AoT has a price.&lt;/strong&gt; No reflection, source-generated JSON, and a careful eye on every dependency. That's good for startup, but it limits what libraries I can pull in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You still need Node.js for some things&lt;/strong&gt; (Sass, LESS, PostCSS, Svelte, Solid, &lt;code&gt;.codegen&lt;/code&gt;). Plain JS / TS / JSX / CSS / HTML bundling needs nothing extra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundler, not type checker.&lt;/strong&gt; As with the other bundlers in this space, I'd keep running &lt;code&gt;tsc&lt;/code&gt; in your CI for actual type checking - Netpack's job is parsing TypeScript and stripping the types, not judging them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Community, Such As It Is
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about this one: Netpack is very early. At the time of writing the repository has &lt;strong&gt;12 stars&lt;/strong&gt; - so if you've read this far, you could be a statistically significant part of the community.&lt;/p&gt;

&lt;p&gt;That also means every contribution counts, and there's plenty of low-hanging fruit: macOS x64 and Windows x86 binaries, framework support (hello, Angular), docs, bug reports from real projects, and benchmarks on your own machines. If you like where this is going, &lt;a href="https://github.com/FlorianRappl/netpack" rel="noopener noreferrer"&gt;star it&lt;/a&gt;, try it, open an issue, or &lt;a href="https://github.com/sponsors/FlorianRappl" rel="noopener noreferrer"&gt;sponsor the project&lt;/a&gt; 🍻. The code is MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next for Netpack
&lt;/h2&gt;

&lt;p&gt;First things first: &lt;strong&gt;1.0&lt;/strong&gt;. The project is still pre-1.0, but I'm positive that I'll get this done by the end of the year - together with a proper release to the community. My 1.0 homework list: closing gaps in the supported scenarios (looking at you, Angular), settling the APIs - including the .NET ones - and bringing documentation and benchmarks up to date.&lt;/p&gt;

&lt;p&gt;I have no illusions, though. The existing bundlers - most importantly Vite as the "meta bundler" - are far too prominent and too well integrated with the ecosystem to be replaced or discarded. Netpack is not going to be your next Vite, and I'm not trying to make it one.&lt;/p&gt;

&lt;p&gt;What makes Netpack shine - and where I see its future - is the integration into .NET. As Netpack is .NET "native" (yes, the application itself is also an AoT artifact, but I mean something else here: it's &lt;em&gt;based&lt;/em&gt; on C#/.NET and can be used as a library in .NET projects), it can be part of ASP.NET Core web servers or build pipelines based on MSBuild, Cake, or Fallout. In any of these cases, many optimizations and use cases are now possible that would have been difficult beforehand.&lt;/p&gt;




&lt;p&gt;That's the Netpack story - from a regular-expression-powered experiment on a winter vacation, via the realization that "to beat native you need native", to a native, AoT-compiled bundler that got fifteen releases in six weeks with a little help from an AI pair programmer. If you'd like to poke around, here are some links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website and documentation:&lt;/strong&gt; &lt;a href="https://netpack.anglevisions.com/" rel="noopener noreferrer"&gt;netpack.anglevisions.com&lt;/a&gt; (&lt;a href="https://netpack.anglevisions.com/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a href="https://github.com/FlorianRappl/netpack" rel="noopener noreferrer"&gt;FlorianRappl/netpack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm (CLI):&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/netpack" rel="noopener noreferrer"&gt;netpack&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;a href="https://www.nuget.org/packages/NetPack.Core" rel="noopener noreferrer"&gt;NetPack.Core&lt;/a&gt; and &lt;a href="https://www.nuget.org/packages/NetPack.Build" rel="noopener noreferrer"&gt;NetPack.Build&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original announcement of the experiment:&lt;/strong&gt; &lt;a href="https://dev.to/florianrappl/netpack-5ej4"&gt;netpack (dev.to)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Previous articles in this series:&lt;/strong&gt; &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;AngleSharp&lt;/a&gt;, &lt;a href="https://dev.to/florianrappl/my-oss-projects-mages-31cl"&gt;Mages&lt;/a&gt;, &lt;a href="https://dev.to/florianrappl/my-oss-projects-piral-2856"&gt;Piral&lt;/a&gt;, and &lt;a href="https://dev.to/florianrappl/my-oss-projects-electronnet-2cgl"&gt;Electron.NET&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up in this series: another project, another origin story. Stay tuned.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My OSS Projects: Electron.NET</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 30 Sep 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/florianrappl/my-oss-projects-electronnet-2cgl</link>
      <guid>https://dev.to/florianrappl/my-oss-projects-electronnet-2cgl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the fourth post in my &lt;strong&gt;"My Open-Source Projects"&lt;/strong&gt; series, where I go through some of the OSS projects I started or maintain and tell you the story behind them. So far we've had &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;AngleSharp&lt;/a&gt;, &lt;a href="https://dev.to/florianrappl/my-oss-projects-mages-31cl"&gt;MAGES&lt;/a&gt;, and &lt;a href="https://dev.to/florianrappl/my-oss-projects-piral-2856"&gt;Piral&lt;/a&gt;. This time: &lt;strong&gt;Electron.NET&lt;/strong&gt; - and it's a bit different, because I didn't start this one. I kind of wandered into the maintainer seat and never left.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is Electron.NET, Actually?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ElectronNET/Electron.NET/" rel="noopener noreferrer"&gt;&lt;strong&gt;Electron.NET&lt;/strong&gt;&lt;/a&gt; lets you build cross-platform desktop apps with .NET - Razor Pages, MVC, Blazor, or even a plain console app - with &lt;a href="https://www.electronjs.org/" rel="noopener noreferrer"&gt;Electron&lt;/a&gt; doing the window-and-pixels part.&lt;/p&gt;

&lt;p&gt;If you've never touched Electron: it's the thing behind a scary number of desktop apps you use every day. It bundles Chromium and Node.js, gives you a window, and lets you build the UI with HTML, CSS, and JavaScript. Yes, that means you ship a browser with your app. No, I'm not going to apologize for that, and neither should you - it works, on Windows, macOS, and Linux, with one codebase.&lt;/p&gt;

&lt;p&gt;The catch, if you're a .NET developer: Electron speaks JavaScript. Electron.NET is the bridge that lets you stay in C#.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Electron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateWindowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BrowserWindowOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Width&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Height&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;768&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 a native desktop window, created from C#. No &lt;code&gt;main.js&lt;/code&gt; in sight.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc7e70bo11bz3z828ayo8.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc7e70bo11bz3z828ayo8.gif" alt="Wait, that's not allowed!" width="498" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Not My Project (Originally)
&lt;/h2&gt;

&lt;p&gt;The other projects in this series all started with me having an idea and then regretting it slowly. Electron.NET is different: it was founded by &lt;strong&gt;Gregor Biswanger&lt;/strong&gt; and &lt;strong&gt;Robert Mühsig&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Their goal will sound familiar if you've read the &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;AngleSharp post&lt;/a&gt;: they wanted a cross-platform GUI framework with C# as the driver. The difference is that they had a big advantage I didn't have back then - they could look at what Electron had already accomplished for Node.js and JavaScript. Robert wrote up the origin story in &lt;a href="https://blog.codeinside.eu/2017/10/31/introducing-electrondotnet/" rel="noopener noreferrer"&gt;a blog post from October 2017&lt;/a&gt;: Gregor asked if it was possible to build desktop apps with ASP.NET Core, they agreed that Electron-as-is plus an embedded ASP.NET Core was the best bet, and then Robert went to bed while Gregor stayed up and built a working prototype. That's the kind of division of labor I can respect.&lt;/p&gt;

&lt;p&gt;Their take was refreshingly simple: &lt;strong&gt;let's use an ASP.NET Core server as the HTML/CSS/JS producer, and wrap the Electron API into an interface that is callable from that server.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works: Three Layers, Two Bridges
&lt;/h2&gt;

&lt;p&gt;Once you accept that idea, you end up with three layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rendering layer (HTML / CSS / JS)&lt;/strong&gt; - the same as always. It talks to Electron through the standard globals and message passing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server layer (C# / ASP.NET Core)&lt;/strong&gt; - produces the UI and talks to Electron through an IPC bridge based on socket.io. (Yes, that's just another flavor of message passing.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js layer (the original Electron trigger code)&lt;/strong&gt; - can be extended with custom code, but is otherwise just the other end of the IPC bridge.&lt;/li&gt;
&lt;/ul&gt;

&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%2Fvk1c1j9bti5wal0tk9n5.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%2Fvk1c1j9bti5wal0tk9n5.png" alt="Electron.NET architecture: a C# / ASP.NET Core server layer connected via a socket.io IPC bridge to the Electron main process (Node.js layer), which connects to the Chromium renderer (HTML/CSS/JS) via message passing; the server also delivers the HTML/CSS/JS to the renderer over localhost" width="799" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your C# code calls something like &lt;code&gt;Electron.WindowManager.CreateWindowAsync(...)&lt;/code&gt;, that call travels over the bridge, the Node.js side makes the actual Electron call, and the result comes back the same way. Meanwhile the renderer happily loads whatever your server serves it. It's a little bit of plumbing, and it's very easy to forget about once it works - which is exactly the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hard Parts
&lt;/h2&gt;

&lt;p&gt;Of course, the idea is the easy part. The journey was (and is) full of questions like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How do you package an app correctly?&lt;/strong&gt; Your app is now a Node.js app, an Electron runtime, &lt;em&gt;and&lt;/em&gt; a .NET application that all need to end up in one installer that works on three operating systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you expose the server - but not too much?&lt;/strong&gt; There's now a web server running on the client's machine. It needs to be reachable by your own window and by nobody else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you debug this?&lt;/strong&gt; Three layers, two runtimes, and a bridge in the middle. "Attach a debugger" turns out to be a much bigger sentence than it sounds like.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5l9cqzqncuisv6kka75.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5l9cqzqncuisv6kka75.gif" alt="One does not simply package an Electron app" width="498" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  electronize, Growing Pains, and 5,000 Stars
&lt;/h2&gt;

&lt;p&gt;To get started, Gregor and Robert invented a system of their own: &lt;strong&gt;electronize&lt;/strong&gt;, a CLI tool that handled building and packaging. It was good enough to begin with, and the framework grew quickly - past 2,000 stars by November 2018, 4,000 by November 2019, and on to 5,000 after that.&lt;/p&gt;

&lt;p&gt;But then the problems stacked up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Outdated versions of Electron&lt;/strong&gt; (which were fixed through electronize, but always somewhat behind)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unresolved issues&lt;/strong&gt; piling up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weird cross-platform bugs&lt;/strong&gt; - the kind that only reproduce on the one OS you don't have at hand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing features&lt;/strong&gt;, mostly from newer Electron versions that the wrapper hadn't caught up with yet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those things kept the project from growing further. And if you look at the classic NuGet package's release history, you can see it. Back then, the major version of Electron.NET &lt;em&gt;was&lt;/em&gt; the Electron compatibility number: Electron.NET 20 matched Electron 20, Electron.NET 13 matched Electron 13, and so on. That's a nice, honest scheme - and it also means every gap in Electron.NET's release history is a gap in which Electron kept moving without you. The releases came with some regularity through 2019, 2020 and 2021 - and then there's nothing on NuGet for 20 months.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqg07clq5i9gdr65i9cp2.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%2Fqg07clq5i9gdr65i9cp2.png" alt="Vertical timeline of Electron.NET on a calendar axis from 2017 to 2026: introduction in October 2017, 2,000 stars in November 2018, 4,000 stars in November 2019, classic ElectronNET.API releases 7.30.2 (Dec 2019), 9.31.2 (Jul 2020), 11.5.1 (Jan 2021) and 13.5.1 (Jul 2021) followed by a 20-month gap, Florian Rappl joining the team and release 23.6.1 in March 2023, 6,000 stars in January 2024, 23.6.2 as the latest classic release in February 2024, ElectronNET.Core launching in November 2025, and ElectronNET.Core 0.6.0 in September 2026" width="800" height="1600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Do the math on 13.5.1 followed by 23.6.1: that's ten Electron major versions in one go.)&lt;/p&gt;

&lt;p&gt;The star curve kept climbing regardless - the repository passed 6,000 stars in January 2024. People clearly wanted this to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stepping In
&lt;/h2&gt;

&lt;p&gt;This is where I stepped in. I joined the team in &lt;strong&gt;March 2023&lt;/strong&gt; - Gregor announced it in &lt;a href="https://github.com/ElectronNET/Electron.NET/issues/744" rel="noopener noreferrer"&gt;issue #744&lt;/a&gt; under the title "Electron.NET Reloaded". I'll be honest: I wasn't planning on becoming a maintainer of a project with thousands of stars. But I'd been in the "web meets .NET" space long enough that the problems looked solvable rather than scary, and together with a few other new maintainers we made a decision that was either brave or reckless: &lt;strong&gt;throw out the tooling and rebuild it from scratch.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The result - launched in &lt;strong&gt;November 2025&lt;/strong&gt; - is &lt;strong&gt;ElectronNET.Core&lt;/strong&gt;: a completely new system, fully based on MSBuild and .NET, supporting essentially any version of Electron. It's also still pre-1.0 (the latest release is 0.6), and the version number no longer means anything about Electron - which is exactly the point.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj5mi38o0jsw0o6svr92u.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%2Fj5mi38o0jsw0o6svr92u.png" alt="Comparison of classic Electron.NET (electronize CLI, electron.manifest.json, Electron-first launch, bundled Electron version, ASP.NET Core required, attach-to-process debugging) versus ElectronNET.Core (MSBuild properties, .NET-first launch, selectable Electron version, ASP.NET optional, .NET-first debugging with Hot Reload and WSL)" width="800" height="343"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What changed, in a nutshell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No more CLI and no more &lt;code&gt;electron.manifest.json&lt;/code&gt;.&lt;/strong&gt; Everything flows through MSBuild project properties, which means Visual Studio's own project system just works. A whole category of "why does my JSON config not do what I think it does" problems disappeared overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET is in control.&lt;/strong&gt; Instead of Electron launching first and babysitting the .NET process, your .NET app launches first and runs Electron as a child process. Better lifecycle management, more reliable shutdown. The Electron-first route still exists, though - there are eight launch scenarios in total (packaged/unpackaged × console/ASP.NET × .NET-first/Electron-first).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Any Electron version.&lt;/strong&gt; No more rigid coupling to whatever version a given release happened to bundle - the old "Electron.NET 20 = Electron 20" scheme is gone. You choose the Electron version with a single property in your project file, and the build validates that it's compatible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ASP.NET is optional.&lt;/strong&gt; A plain console app is now enough if all you need is to load HTML from the file system or from a remote server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging that feels like .NET.&lt;/strong&gt; Press F5, get breakpoints and Hot Reload. You can even build and debug the Linux version of your app from Windows Visual Studio through WSL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full API compatibility.&lt;/strong&gt; Existing code keeps working - including custom host hooks - so migrating is mostly about the project setup, not a rewrite. There's a &lt;a href="https://github.com/ElectronNET/Electron.NET/wiki/Migration-Guide" rel="noopener noreferrer"&gt;migration guide&lt;/a&gt; for exactly that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The new package structure is intentionally modular:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ElectronNET.Core&lt;/code&gt; - the main package with the build logic and project system integration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ElectronNET.Core.API&lt;/code&gt; - the pure API definitions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ElectronNET.Core.AspNet&lt;/code&gt; - the ASP.NET-specific runtime pieces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ElectronNET.Core.Templates&lt;/code&gt; - project templates to get started quickly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F122ovu7ttkhsikt9elln.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F122ovu7ttkhsikt9elln.gif" alt="We rewrote the build system and everything still works" width="498" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;You'll need .NET 8 or 10 and Node.js 22 or newer. For an ASP.NET-based app, create a new ASP.NET Core project and add two packages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package ElectronNET.Core
dotnet add package ElectronNET.Core.AspNet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then hook Electron into the startup of a minimal API (Razor Pages here, but MVC works the same way):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ElectronNET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ElectronNET.API&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ElectronNET.API.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRazorPages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddElectron&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// handy for dependency injection&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseElectron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Electron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateWindowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BrowserWindowOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Show&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AutoHideMenuBar&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnReadyToShow&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseStaticFiles&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseRouting&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapRazorPages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the callback you pass to &lt;code&gt;UseElectron()&lt;/code&gt; - it tells you the right moment to set up your UI, which used to be a source of mysterious race conditions.&lt;/p&gt;

&lt;p&gt;Then press &lt;strong&gt;F5&lt;/strong&gt;. That's it. You're running a desktop app.&lt;/p&gt;

&lt;p&gt;Want a specific Electron version? It's just an MSBuild property in your &lt;code&gt;.csproj&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;PropertyGroup&lt;/span&gt; &lt;span class="na"&gt;Label=&lt;/span&gt;&lt;span class="s"&gt;"ElectronNetCommon"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ElectronVersion&amp;gt;&lt;/span&gt;30.4.0&lt;span class="nt"&gt;&amp;lt;/ElectronVersion&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That's the default in the docs at the time of writing - the &lt;a href="https://github.com/ElectronNET/Electron.NET/wiki/Configuration" rel="noopener noreferrer"&gt;configuration page&lt;/a&gt; lists all the other settings.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Blazor
&lt;/h3&gt;

&lt;p&gt;For Blazor there's exactly one thing to remember - the &lt;code&gt;IsRunningBlazor&lt;/code&gt; option, which sets up the renderer in a way that Blazor can run without interference (including hot module replacement during development):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseElectron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BrowserWindowOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Show&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IsRunningBlazor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- crucial&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Electron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateWindowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnReadyToShow&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&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;h3&gt;
  
  
  No ASP.NET? No Problem.
&lt;/h3&gt;

&lt;p&gt;If you don't need a local web server at all, a console app is enough. Create one with &lt;code&gt;dotnet new console&lt;/code&gt;, add &lt;code&gt;dotnet add package ElectronNET.Core&lt;/code&gt;, and start the runtime yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ElectronNET.API&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;ElectronNET.API.Entities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runtimeController&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ElectronNetRuntime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RuntimeController&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runtimeController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runtimeController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitReadyTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Electron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WindowManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateWindowAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BrowserWindowOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Show&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// file system, remote URL, whatever you like&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WebContents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LoadURLAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnReadyToShow&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runtimeController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WaitStoppedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(This is a trimmed version - the &lt;a href="https://github.com/ElectronNET/Electron.NET/wiki/Console-App" rel="noopener noreferrer"&gt;console app page in the wiki&lt;/a&gt; has the complete example including the project file settings you need.)&lt;/p&gt;

&lt;h3&gt;
  
  
  And the Rest of Electron?
&lt;/h3&gt;

&lt;p&gt;The C# API covers a good chunk of what Electron offers: windows, menus, dialogs, the tray, notifications, the clipboard, global shortcuts, the auto-updater, native theme, power monitor, screens, &lt;code&gt;ipcMain&lt;/code&gt;, and more. The &lt;a href="https://github.com/ElectronNET/Electron.NET/wiki" rel="noopener noreferrer"&gt;wiki&lt;/a&gt; has a page for each of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You keep your stack.&lt;/strong&gt; If you know ASP.NET Core - Razor Pages, MVC, or Blazor - you already know 90% of what it takes to build a desktop app. The web skills and the C# skills you have are the skills you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real cross-platform, one codebase.&lt;/strong&gt; Windows, macOS, and Linux, with the packaging story handled by MSBuild instead of a separate tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible architecture.&lt;/strong&gt; Full ASP.NET host, or a lightweight console app that loads local files or a remote URL. The setup fits the app, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern debugging.&lt;/strong&gt; .NET-first launch, Hot Reload, and WSL for Linux builds is a workflow that just feels like normal .NET development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Electron ecosystem is right there.&lt;/strong&gt; If it works in Electron, it works in Electron.NET - and if there's a custom bit of JavaScript you need, the Node.js layer can be extended with your own code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Struggles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's Electron.&lt;/strong&gt; You're shipping Chromium &lt;em&gt;and&lt;/em&gt; a .NET runtime &lt;em&gt;and&lt;/em&gt; Node.js. Installers are big, memory usage is what you'd expect from a browser plus a server, and if your app is a small utility, a native UI toolkit might be a better fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two ecosystems to keep in your head.&lt;/strong&gt; Even with .NET in the driver's seat, you still touch Node.js (which is a requirement), and now and then the JavaScript side of the story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The API is a wrapper.&lt;/strong&gt; That's a strength (you get C# instead of JS) but also a limitation: a shiny new Electron feature has to be exposed in the C# API before you can use it comfortably. Keeping up with that is part of what stalled the classic line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core is young.&lt;/strong&gt; It launched in November 2025 and is still pre-1.0 (latest release: 0.6). It's moving fast - new pre-release builds show up almost daily - but "fast-moving" and "settled" are not the same thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrating from classic takes some effort.&lt;/strong&gt; Full API compatibility helps a lot, but the project setup is different, and you'll have to work through the migration guide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The team is small.&lt;/strong&gt; This is open-source work done in our free time. More on that below.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f7s5ygg0eom34seamkz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9f7s5ygg0eom34seamkz.gif" alt="It's not perfect but it's ours" width="498" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  People Behind It
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://github.com/ElectronNET/Electron.NET/#-authors" rel="noopener noreferrer"&gt;README&lt;/a&gt; lists four authors: &lt;strong&gt;Gregor Biswanger&lt;/strong&gt; and &lt;strong&gt;Robert Mühsig&lt;/strong&gt; (the founders), &lt;strong&gt;softworkz&lt;/strong&gt;, and me.&lt;/p&gt;

&lt;p&gt;The current changelog reads like a thank-you note to everyone who showed up: &lt;strong&gt;softworkz&lt;/strong&gt; has shipped an enormous share of the Core work (infrastructure, build system, integration tests - the list is long), while &lt;strong&gt;agracio&lt;/strong&gt;, &lt;strong&gt;AeonSake&lt;/strong&gt;, &lt;strong&gt;Denny09310&lt;/strong&gt;, &lt;strong&gt;NimbusFox&lt;/strong&gt;, &lt;strong&gt;davidroth&lt;/strong&gt;, &lt;strong&gt;markatosi&lt;/strong&gt;, &lt;strong&gt;DYH1319&lt;/strong&gt;, &lt;strong&gt;hilin&lt;/strong&gt; and &lt;strong&gt;adityashirsatrao007&lt;/strong&gt; all appear with fixes, features, docs, and sample apps.&lt;/p&gt;

&lt;p&gt;Since this is done in our free time, there's a &lt;a href="https://donorbox.org/electron-net" rel="noopener noreferrer"&gt;donation page&lt;/a&gt;, and you can also sponsor the core maintainers on GitHub (&lt;a href="https://github.com/sponsors/GregorBiswanger" rel="noopener noreferrer"&gt;Gregor&lt;/a&gt;, &lt;a href="https://github.com/sponsors/FlorianRappl" rel="noopener noreferrer"&gt;me&lt;/a&gt;) - or &lt;a href="https://buymeacoffee.com/florianrappl" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;. Donations can even be used to bump the priority of an issue. For questions you can use &lt;a href="https://github.com/ElectronNET/Electron.NET/discussions" rel="noopener noreferrer"&gt;the GitHub discussions feature&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Electron.NET Shows Up Today
&lt;/h2&gt;

&lt;p&gt;The numbers first: the repository has &lt;strong&gt;7.6k stars&lt;/strong&gt; and &lt;strong&gt;741 forks&lt;/strong&gt;. The classic &lt;code&gt;ElectronNET.API&lt;/code&gt; package sits at over &lt;strong&gt;650K downloads&lt;/strong&gt;, and the much younger Core packages have already crossed &lt;strong&gt;40K&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Electron.NET is widely used - for demos, within Microsoft, and within the .NET community. Some of the projects built on top of it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/ChangemakerStudios/Papercut-SMTP" rel="noopener noreferrer"&gt;Papercut SMTP&lt;/a&gt;&lt;/strong&gt; (3.3K stars), the simple desktop email server that developers use to catch outgoing mail during testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/domialex/Sidekick" rel="noopener noreferrer"&gt;Sidekick&lt;/a&gt;&lt;/strong&gt;, a helper app for the game Path of Exile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Texnomic/SecureDNS" rel="noopener noreferrer"&gt;SecureDNS&lt;/a&gt;&lt;/strong&gt;, an all-in-one cross-platform DNS server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Jinjinov/Ididit" rel="noopener noreferrer"&gt;Ididit&lt;/a&gt;&lt;/strong&gt;, a Blazor habit tracker that runs on the web and on desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/hyprismteam/Hyprism" rel="noopener noreferrer"&gt;Hyprism&lt;/a&gt;&lt;/strong&gt;, an open-source Hytale launcher - built on the new ElectronNET.Core.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/aesalazar/AsteroidsWasm" rel="noopener noreferrer"&gt;AsteroidsWasm&lt;/a&gt;&lt;/strong&gt;, a single C# project that runs as Blazor WASM, Blazor Server, Electron, WPF, WinForms, MAUI, and WinUI3 (which has to be some kind of record).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that was in the plan when Gregor stayed up to hack together a prototype, but "let .NET developers use the web stack they already know for desktop apps" turns out to be a pretty good idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next for Electron.NET
&lt;/h2&gt;

&lt;p&gt;The Core rewrite removed the technical debt that was holding the project back. The flexible Electron versioning, the integrated build system, and the cross-platform tooling are the foundation for more frequent updates, better tooling and IDE integration, easier community contributions, and support for more platforms. Newer Electron versions are already supported, and the release train is running.&lt;/p&gt;

&lt;p&gt;The future certainly holds more treats. I'd tell you what, but I'm not great at keeping surprises - so I'll just say: watch the repository.&lt;/p&gt;




&lt;p&gt;That's the Electron.NET story - from a late-night prototype in 2017, through 5,000 stars and a rough patch, to a complete rebuild on top of MSBuild and .NET. If you'd like to poke around, here are some links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub project:&lt;/strong&gt; &lt;a href="https://github.com/ElectronNET/Electron.NET/" rel="noopener noreferrer"&gt;ElectronNET/Electron.NET&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NuGet:&lt;/strong&gt; &lt;a href="https://www.nuget.org/packages/ElectronNET.Core.API/" rel="noopener noreferrer"&gt;ElectronNET.Core.API&lt;/a&gt; (new, rework) and &lt;a href="https://www.nuget.org/packages/ElectronNET.API" rel="noopener noreferrer"&gt;ElectronNET.API&lt;/a&gt; (classic)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation / what's new:&lt;/strong&gt; &lt;a href="https://github.com/ElectronNET/Electron.NET/wiki/What's-New" rel="noopener noreferrer"&gt;What's New&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Previous articles in this series:&lt;/strong&gt; &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;AngleSharp&lt;/a&gt;, &lt;a href="https://dev.to/florianrappl/my-oss-projects-mages-31cl"&gt;Mages&lt;/a&gt;, and &lt;a href="https://dev.to/florianrappl/my-oss-projects-piral-2856"&gt;Piral&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next up in this series: another project, another origin story. Stay tuned.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>electron</category>
      <category>csharp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My OSS Projects: Piral</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 23 Sep 2026 10:59:45 +0000</pubDate>
      <link>https://dev.to/florianrappl/my-oss-projects-piral-2856</link>
      <guid>https://dev.to/florianrappl/my-oss-projects-piral-2856</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the third post in my &lt;strong&gt;"My Open-Source Projects"&lt;/strong&gt; series, where I go through some of the OSS projects I started or maintain and tell you the story behind them. &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;First up was AngleSharp&lt;/a&gt;, then &lt;a href="https://dev.to/florianrappl/my-oss-projects-mages-31cl"&gt;MAGES&lt;/a&gt;. This time: &lt;strong&gt;Piral&lt;/strong&gt;, a framework for micro frontends - and the first project in this series that came out of a client engagement rather than a plane ride or a game studio's grant.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is Piral, Actually?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://piral.io/" rel="noopener noreferrer"&gt;&lt;strong&gt;Piral&lt;/strong&gt;&lt;/a&gt; lets you build a frontend application as a stable &lt;strong&gt;app shell&lt;/strong&gt; that gets extended at runtime by independently developed, independently deployed modules called &lt;strong&gt;pilets&lt;/strong&gt;. A pilet ships its own code and assets, can register pages, extend other pilets' extension points, and can be built, tested, and released completely on its own team's schedule - without anyone needing to touch or even redeploy the shell.&lt;/p&gt;

&lt;p&gt;If that sounds like "micro frontends," that's because it is. Piral was one of the earlier dedicated frameworks in that space, built specifically around the idea that a single main framework (usually React) drives the shell, while individual pilets are free to bring something else entirely if they need to - Vue, Angular, even something short-lived and purpose-built. The shell doesn't care. It only cares about the contract.&lt;/p&gt;

&lt;p&gt;I've written a more detailed technical introduction to the concepts before, if you want the deep dive: &lt;a href="https://dev.to/florianrappl/introduction-to-microfrontends-with-piral-4mpp"&gt;Introduction to Microfrontends with Piral&lt;/a&gt;. This post is more about where it came from and how it's grown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Actually Started: A German Energy Company, Then ZEISS
&lt;/h2&gt;

&lt;p&gt;Piral wasn't born as "let's make a framework." It was born as "let's actually ship this customer portal, for real this time."&lt;/p&gt;

&lt;p&gt;I'd just come off a large rewrite of the smart-home portal for a large German energy company, where a micro-service-backed, loosely-coupled-frontend approach worked far better than anyone expected. The backend teams had already gone the micro-services route for their own reasons - scaling, ownership, independent release cycles - and it seemed almost wasteful to let all of that independence evaporate the moment it hit the frontend, where everything got welded back together into one shared codebase anyway. So we didn't. Multiple teams could ship independently without stepping on each other - which, if you've ever worked on a large frontend monolith with a dozen teams committing to the same &lt;code&gt;App.tsx&lt;/code&gt;, is not a sentence you get to say often.&lt;/p&gt;

&lt;p&gt;Then I became the main architect for a new digital customer portal at &lt;strong&gt;ZEISS&lt;/strong&gt; - a large German company that had, by that point, tried and failed to pull off exactly this kind of portal for years. Not for lack of trying, and not for lack of good engineers - it's the kind of project that accumulates false starts simply because "let many teams contribute to one coherent customer-facing app" is a genuinely hard organizational and technical problem, and prior attempts had all run into the same wall: a single frontend codebase that every team had to touch, coordinate around, and eventually avoid touching at all costs.&lt;/p&gt;

&lt;p&gt;I noticed the parallels to what had just worked at the RWE SmartHome project and leaned into the same approach: micro-service backends paired with what we'd now call micro frontends, even though nobody was calling it that yet - the term hadn't really entered common usage. It worked. Multiple teams contributing at what genuinely felt like the speed of thought, instead of the speed of "please rebase your branch against the shared frontend repo again, and also here's a merge conflict in a file six people touched this week."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnmd0iy4bid2vf0e1onez.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnmd0iy4bid2vf0e1onez.gif" alt="Wait, that actually works?" width="498" height="172"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At that point the obvious question was: why keep reinventing this per client? Why not generalize the approach into an actual framework?&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming It (And Accidentally Foreshadowing Something)
&lt;/h2&gt;

&lt;p&gt;I joined &lt;strong&gt;smapiot&lt;/strong&gt; in February 2019, and the direction was clear from day one: alongside the regular consulting work, there'd be dedicated time to build a framework for micro frontends. By March 2019 we'd settled on a name: &lt;strong&gt;Piral&lt;/strong&gt;, short for "Portals that can go viral." A year later, the "viral" part of that name aged in a way absolutely nobody could have planned for. We still bring it up.&lt;/p&gt;

&lt;p&gt;We officially revealed Piral at the &lt;strong&gt;O'Reilly Software Architecture Conference Berlin&lt;/strong&gt; in November 2019. In hindsight, that conference turned out to be the last one O'Reilly ran in that series - it never picked back up after COVID hit, at least as far as I know. So Piral's public debut happened at what turned out to be the end of an era for that particular conference. The response at the time, though, was genuinely great - people wanted to try it immediately, which is not always guaranteed when you show up with "yet another frontend framework" at a software architecture conference. We'd braced ourselves for the usual round of "how is this different from an iframe" questions, and got a refreshing number of "can we pilot this next quarter" ones instead.&lt;/p&gt;

&lt;p&gt;Here's the whole arc at a glance, from that first month at smapiot to where things stand now:&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqxqdgyt8mo0blav28y5m.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%2Fqxqdgyt8mo0blav28y5m.png" alt="Piral timeline on a real calendar axis from 2019 to 2026, showing joining smapiot and naming Piral in early 2019, the O'Reilly Berlin reveal in November 2019, Piral Inspector's release in February 2020, the Feed Service reaching 1.0.0 in October 2021, Piral.Blazor v3 in September 2022, Piral v1.0 alongside the first Micro Frontends Conference in June 2023, native Module Federation support in Piral 1.4.0 that December, Piral.Blazor.Server targeting .NET 8 in February 2024, the 2024 conference in June, Piral 1.8.0 reaching full ecosystem maturity in January 2025, the Feed Service reaching 1.17.0 that October, and today's mature, stable ecosystem in production at thousands of places" width="800" height="1320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Seven years is a long time for a framework built around one client project's success to still be actively developed, still picking up new adopters, and still running its own annual conference. None of that was guaranteed back in that Berlin conference room in 2019. A couple of things worth calling out about the timeline itself: the gaps are real - not every quarter had a headline-worthy event, and I'd rather show honest quiet stretches than pad the axis with filler. And two of the dates lined up in a way I didn't plan: Piral's v1.0 release and the very first Micro Frontends Conference both landed in June 2023, on the same day, which made for a genuinely good excuse for a celebration.&lt;/p&gt;

&lt;p&gt;A few of those milestones are worth a beat of their own. The &lt;strong&gt;Feed Service&lt;/strong&gt; has its own steady release cadence running quietly alongside the framework - 1.0.0 in October 2021, 1.17.0 four years later. &lt;strong&gt;Piral.Blazor&lt;/strong&gt; turned into a small ecosystem of its own, from v3 in September 2022 to a dedicated server-side variant targeting .NET 8 in February 2024. And &lt;strong&gt;native Module Federation support landing in Piral 1.4.0&lt;/strong&gt; (December 2023) was a quiet but important one - it meant Piral could sit alongside, rather than only compete with, one of the other major approaches to shipping micro frontends.&lt;/p&gt;

&lt;p&gt;Worth calling out explicitly: the "hundreds of pilets in one app" scaling story isn't something Piral grew into over these seven years - it was a design goal from the very first version, informed directly by watching what broke at scale in that original ZEISS-style setup. What's changed since 2019 isn't the ceiling; it's how many production systems are actually pushing against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not Just Theory: Real Adoption, Including Outside Piral Itself
&lt;/h2&gt;

&lt;p&gt;The consulting projects that followed proved the concept held up outside of ZEISS's specific context, which is always the real test for something that started as "a good idea from one project." It's one thing to make an architecture work when you're the one who designed it and you're in the room for every decision. It's another to hand it to a different team, on a different project, with different constraints, and watch it hold up anyway. Piral did.&lt;/p&gt;

&lt;p&gt;What surprised me more, though, was interest from teams that weren't using Piral at all. Larger projects already built on &lt;strong&gt;Single-SPA&lt;/strong&gt; started reaching out for help getting Piral's loosely-coupled integration patterns into their existing workflows. They didn't want to migrate away from code that already worked - nobody sane wants a rewrite of a production system that's doing its job - but they wanted the &lt;em&gt;ideas&lt;/em&gt; behind Piral (the decoupling model, the independent deployability, the way pilets could be updated and rolled out without a full redeploy) layered onto the micro frontend setup they already had. In practice, that meant bringing over specific mechanisms - things like our approach to shared dependencies, or the update/rollout model - without asking anyone to throw away months or years of Single-SPA work.&lt;/p&gt;

&lt;p&gt;That's a good sign for any architecture: when people want to borrow your patterns even while keeping their own stack. It's a much stronger signal than stars or downloads, because it means the ideas are portable even when the code isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Ecosystem, Not Just a Library
&lt;/h2&gt;

&lt;p&gt;From the start, Piral was meant to be more than "install a package, get a framework." Different pieces of tooling were deliberately built as their own generic, reusable things - not tightly bolted to Piral internals. That's a design choice, not an accident: a debugging tool that only understands Piral-specific concepts is a much smaller investment than one built around general primitives that Piral happens to use too, but the latter is far more likely to survive the framework's own evolution - and far more useful to anyone standing outside the Piral world entirely.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foah4abdqp1fbffqm9hfd.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%2Foah4abdqp1fbffqm9hfd.png" alt="Piral app shell diagram: independently shipped React, Vue, Angular, and vanilla JS pilets all connecting into a single app shell, with an optional feed service handling discovery and delivery" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The clearest example is the &lt;strong&gt;&lt;a href="https://github.com/smapiot/piral-inspector" rel="noopener noreferrer"&gt;Piral Inspector&lt;/a&gt;&lt;/strong&gt;, a browser DevTools extension for inspecting and debugging a running Piral instance - which pilets are loaded, what routes are registered, what dependencies are shared, toggling settings like state-container logging, and even loading new pilets on the fly from a root module URL, a feed URL, or a tarball for quick local testing. It's built with an adapter pattern specifically so its core debugging plumbing (a background script talking to a content script talking to a devtools panel - the standard shape for any browser extension that needs to inspect a page) isn't locked to Piral-only scenarios. It ships for Firefox, Chrome, Opera, and Edge, which is more browser coverage than most internal dev tools ever bother with, and it's a genuinely small, focused codebase - the kind of tool you can actually read end to end in an afternoon if you're curious how it talks to a running app.&lt;/p&gt;

&lt;p&gt;Beyond that, the ecosystem includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;&lt;a href="https://github.com/smapiot/vscode-piral" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt;&lt;/strong&gt; for authoring pilets and app shells with proper tooling support instead of raw text editing and hope.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;templating system&lt;/strong&gt; (via the CLI) to scaffold a new pilet or a whole new app shell in seconds, rather than copy-pasting a previous project and deleting things until it works.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Piral Cloud Feed Service&lt;/strong&gt; - the commercial piece that ties it all together.&lt;/li&gt;
&lt;/ul&gt;

&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%2F6u45tzft1ss137uf5vbj.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%2F6u45tzft1ss137uf5vbj.png" alt="Piral ecosystem diagram: Piral Inspector, VS Code extension, and templates/CLI feeding into the core framework, with Piral Cloud Feed, an on-premise feed alternative, and the Discord community and conference as outputs" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feed Service: The Giant Everything Stands On (But Isn't Required)
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href="https://piral.cloud/" rel="noopener noreferrer"&gt;Piral Cloud Feed Service&lt;/a&gt;&lt;/strong&gt; is, in practice, the thing that makes rolling out and updating micro frontends at scale actually pleasant - it's how pilets get discovered, versioned, and delivered to app shells without anyone manually wiring up a deployment pipeline per team. Push a new pilet version, the feed picks it up, connected app shells get the update - no rebuild of the shell, no coordinated release train, no waiting for the next sprint's deployment window.&lt;/p&gt;

&lt;p&gt;Here's the part I think matters most, philosophically: &lt;strong&gt;the Piral framework has no hard dependency on the Feed Service.&lt;/strong&gt; We built it that way on purpose, from day one, even though it would have been the easier (and more commercially convenient) choice to couple them tightly. How you distribute and update your pilets is entirely up to you - and even if you decide a discovery/delivery service is the right call (it usually is, once you have more than a couple of teams shipping independently), it doesn't have to be ours. Any micro frontend discovery mechanism can plug into the same contract, because the contract itself is just "here's a list of pilets and where to fetch them" - deliberately unglamorous, deliberately not locked to one vendor.&lt;/p&gt;

&lt;p&gt;Do I think the Piral Cloud Feed Service is the best option out there? Yes, obviously - I'm somewhat biased, having helped build it, and I've seen what it looks like when teams try to roll their own version of the same thing from scratch. But that's a big enough topic for its own article rather than a paragraph here. If you want to explore it yourself: the &lt;a href="https://piral.cloud/" rel="noopener noreferrer"&gt;landing page&lt;/a&gt; has the overview, &lt;a href="https://portal.piral.cloud/" rel="noopener noreferrer"&gt;portal.piral.cloud&lt;/a&gt; is where you log into the free community tier, and the licensed version runs fully on-premise for anyone who needs that - a common requirement once you're talking to companies with strict data-residency rules.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvt1cjehcaa7x8ki0xux.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpvt1cjehcaa7x8ki0xux.gif" alt="We could have locked you in, but we didn't" width="498" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a Feel for It
&lt;/h2&gt;

&lt;p&gt;Scaffolding a new app shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx piral new my-shell
&lt;span class="nb"&gt;cd &lt;/span&gt;my-shell
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a new pilet that plugs into it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx pilet new &lt;span class="nt"&gt;--source&lt;/span&gt; my-shell
&lt;span class="nb"&gt;cd &lt;/span&gt;my-pilet
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a pilet, registering a page looks like this:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PiletApi&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;my-shell&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;piral&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PiletApi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;piral&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/hello&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Hello&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;pilet&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="err"&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 genuinely most of it for a minimal case. The &lt;code&gt;PiletApi&lt;/code&gt; surface is where the interesting extensibility lives - registering pages, extension slots, shared state, notifications, menu items, and more, all without the shell needing to know a given pilet exists ahead of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loose coupling as a first-class citizen, not an afterthought.&lt;/strong&gt; Pilets are isolated by design - built, versioned, tested, and deployed independently. A broken pilet release doesn't take down the shell or other pilets, and a team can ship a hotfix to their one pilet at 4pm on a Friday without needing sign-off from anyone outside their own team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It genuinely scales to a lot of micro frontends.&lt;/strong&gt; This is the one I'd highlight hardest: Single-SPA-style setups tend to get uncomfortable somewhere around 10 micro frontends, and Module Federation setups can typically stretch to 30-40 depending on how deep the sub-module graph gets before the shared-dependency graph turns into its own puzzle to manage. Piral is, as far as I'm aware, the only micro frontend framework built to comfortably scale into the &lt;strong&gt;hundreds&lt;/strong&gt; of pilets in a single application - which matters a great deal once you're not talking about a handful of teams anymore, but dozens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework-agnostic at the pilet level.&lt;/strong&gt; React drives most shells, but a pilet can be Vue, Angular, or something else entirely - useful for legacy migration paths (rewrite one piece at a time instead of a big-bang cutover) or short-lived experiments that shouldn't need to match the shell's stack just to get evaluated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's an actual ecosystem.&lt;/strong&gt; Devtools, editor tooling, scaffolding, and a discovery/delivery layer - all usable independently, and none of it locked behind "you must use our cloud service." That's a rarer design principle than it should be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not vendor-locked to its own commercial offering.&lt;/strong&gt; The Feed Service being fully optional, by design, is a rarer stance than you'd think for a company that also sells a feed service - it would have been easier, short-term, to make the two inseparable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Struggles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smaller community than the bigger names in the space.&lt;/strong&gt; Compared to Module Federation or Single-SPA, Piral's community is meaningfully smaller - even though (or maybe partly because) it's used at some genuinely large companies who don't always talk publicly about their stack. Enterprise adoption doesn't show up as GitHub stars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React and React Router are still baked in, for now.&lt;/strong&gt; Piral v1's core assumes React and React Router underneath, even though you can swap either out if you're willing to do the work (there are converter packages for Vue, Angular, and others at the pilet level, but the shell's assumptions run deeper). That's changing - Piral v2 is dropping both as hard prerequisites from the ground up - but today, going off the beaten path takes more effort than it should.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The learning curve is real if you're new to micro frontends as a concept.&lt;/strong&gt; Pilets, extension slots, shared dependencies, feed services - there's genuine architectural thinking required before you ship your first production app shell. This isn't a "npm install and you're basically done" kind of framework, and it was never trying to be one; the complexity it removes downstream (independent team deployments) has to be paid for somewhere, and that's mostly upfront in understanding the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The commercial Feed Service, while optional, is where a lot of the "batteries included" experience lives.&lt;/strong&gt; Rolling your own discovery/delivery mechanism is entirely possible, but you're signing up to build and maintain that piece yourself - versioning, rollout logic, health checks, the works.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6u2cpu3aqtmps8fe99z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp6u2cpu3aqtmps8fe99z.gif" alt="with great flexibility comes great setup time" width="498" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Community: Smaller, But Loud in the Right Rooms
&lt;/h2&gt;

&lt;p&gt;Piral has never chased npm download charts, and it shows - but it's shown up consistently where the actual micro frontends conversation happens. We've hosted numerous community talks over the years, at conferences and meetups well beyond our own events, and since 2023 we've run the &lt;strong&gt;&lt;a href="https://conference.microfrontends.cloud/" rel="noopener noreferrer"&gt;Micro Frontends Conference&lt;/a&gt;&lt;/strong&gt; annually - a free, one-day virtual event with 15+ speakers, several of whom are genuinely foundational names in this space (people who've built or shaped Module Federation, Single-SPA, and other tools you've likely already used).&lt;/p&gt;

&lt;p&gt;The 2024 edition ran June 17th; sponsors across the two editions included JetBrains and several others from across the ecosystem. It's not a Piral-only event by design - the point was, and is, the micro frontends community as a whole, not just promoting our own framework. Running a conference that deliberately platforms competing approaches is a strange thing to do if your only goal is selling your own framework; it's a much more natural thing to do if your actual goal is a healthier ecosystem, which - vested interest aside - is genuinely the goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next for Piral
&lt;/h2&gt;

&lt;p&gt;The big one on the horizon is &lt;strong&gt;Piral v2&lt;/strong&gt;, which drops the historical React and React Router requirements from day one instead of treating framework-agnosticism as something you bolt on with extra converters. That's a meaningful shift for a framework that's been React-first since its earliest versions - it means the core assumptions about rendering and routing get rethought rather than patched around, which is a bigger undertaking than it sounds like from one sentence.&lt;/p&gt;

&lt;p&gt;Beyond that, the roadmap keeps circling back to making the "hundreds of pilets" scaling story even smoother as real deployments push past what we originally validated it against, and continuing to grow the ecosystem pieces (inspector, editor tooling, templates) independently of the core - the same philosophy that's been there since the beginning: build pieces that are useful even outside of a strict "you must use all of Piral" context. If the last seven years are any indication, the interesting adoption stories will keep coming from places we didn't specifically design for.&lt;/p&gt;




&lt;p&gt;That's the Piral story - from a customer portal at ZEISS that finally worked, to a framework that scales to hundreds of independently shipped pilets, to hosting its own community conference. If you want to poke around: the &lt;a href="https://piral.io/" rel="noopener noreferrer"&gt;website&lt;/a&gt; has the overview, the &lt;a href="https://github.com/smapiot/piral" rel="noopener noreferrer"&gt;repository&lt;/a&gt; has the code, and the &lt;a href="https://dev.to/florianrappl/introduction-to-microfrontends-with-piral-4mpp"&gt;introduction article&lt;/a&gt; has the deeper technical walkthrough.&lt;/p&gt;

&lt;p&gt;Next up in this series: another project, another origin story. Stay tuned.&lt;/p&gt;

</description>
      <category>microfrontends</category>
      <category>react</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My OSS Projects: Mages</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 16 Sep 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/florianrappl/my-oss-projects-mages-31cl</link>
      <guid>https://dev.to/florianrappl/my-oss-projects-mages-31cl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the second post in my &lt;strong&gt;"My Open-Source Projects"&lt;/strong&gt; series, where I go through some of the OSS projects I started or maintain and tell you the story behind them. &lt;a href="https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j"&gt;Last time it was AngleSharp&lt;/a&gt; - the HTML parser that started life as a plane-ride fever dream. This time: &lt;strong&gt;MAGES&lt;/strong&gt;, a much smaller project with a surprisingly cool client list.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is MAGES, Actually?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/FlorianRappl/Mages" rel="noopener noreferrer"&gt;&lt;strong&gt;MAGES&lt;/strong&gt;&lt;/a&gt; - officially standing for "Mages: Another Generalized Expression Simplifier," because every good project needs a slightly forced backronym - is a small, fast expression parser and interpreter for .NET. You hand it a string like &lt;code&gt;"sin(2) * cos(pi / 4)"&lt;/code&gt;, it hands you back a number. Except it also does variables, functions, closures, objects, lists, complex numbers, and - as of a couple of years ago - a bit of JSX for fun.&lt;/p&gt;

&lt;p&gt;Think of it as "give your .NET app a tiny, embeddable scripting language" without dragging in a full-blown runtime, a NuGet dependency tree the size of a phone book, or a licensing conversation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Mages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sin(2) * cos(pi / 4)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0.642970376623918&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole "hello world." No config, no ceremony.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7g8h9c0p86jv1keoffg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz7g8h9c0p86jv1keoffg.gif" alt="It just works" width="640" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The YAMP Prehistory
&lt;/h2&gt;

&lt;p&gt;MAGES didn't start as MAGES. It started as &lt;a href="https://github.com/FlorianRappl/YAMP" rel="noopener noreferrer"&gt;&lt;strong&gt;YAMP&lt;/strong&gt;&lt;/a&gt; - "Yet Another Math Parser" - an experiment to see whether I could build a full parser and evaluator with MATLAB-like syntax, powered almost entirely by reflection. Reflection made it wonderfully easy to extend: want a new function? Slap an attribute on a method and YAMP would pick it up. It was never meant to be fast. It was meant to be &lt;em&gt;flexible&lt;/em&gt;, and as far as experiments go, it worked.&lt;/p&gt;

&lt;p&gt;YAMP ended up seeing more real usage than an "experiment" really has any right to. The best example: it powered the math engine behind &lt;strong&gt;SineRider&lt;/strong&gt;, a Unity web game from 2013 where you graph functions to sled a character through checkpoints - think Line Rider, but the slope comes from an equation you type in, not a mouse drag. It's a delightful bit of edutainment, and knowing that YAMP's reflection-driven, definitely-not-optimized-for-speed parser was quietly evaluating someone's sine wave in a browser game is exactly the kind of thing that makes open source fun.&lt;/p&gt;

&lt;p&gt;If you want to see what that original version actually looked like, &lt;a href="https://www.youtube.com/watch?v=9FU103w2EWg" rel="noopener noreferrer"&gt;there's footage of it right here&lt;/a&gt; - very much a product of its era. The good news is you don't have to rely on old footage: after the original Unity Web Player build was killed off by browsers dropping NPAPI support, a team of teenagers at &lt;a href="https://hackclub.com/" rel="noopener noreferrer"&gt;Hack Club&lt;/a&gt; rebuilt SineRider from scratch in vanilla JavaScript, and it's &lt;a href="https://sinerider.com/" rel="noopener noreferrer"&gt;still there, still free, still very playable today&lt;/a&gt;. Current sources live at &lt;a href="https://github.com/hackclub/sinerider" rel="noopener noreferrer"&gt;hackclub/sinerider&lt;/a&gt; if you're curious how it's built these days (spoiler: no MAGES or YAMP in there anymore - it's its own thing now).&lt;/p&gt;

&lt;h2&gt;
  
  
  From Experiment to Grant-Funded Successor
&lt;/h2&gt;

&lt;p&gt;Here's where the story gets a step more interesting than "I wrote a parser on a plane" (that's a different article; see first part of the series). The person behind SineRider wanted to go bigger: not just a game about graphing one function, but a whole world built on the idea that everything - terrain, objects, physics - could be governed by changeable math functions. Picture Minecraft, except the blocks are mutable equations. That's a genuinely ambitious pitch, and it was ambitious enough to catch a &lt;strong&gt;Peter Thiel grant&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Part of that grant went toward funding the next generation of the expression engine underneath it. YAMP's syntax could carry over, but its reflection-based guts were never going to be fast enough for a real-time, function-driven game world. So MAGES was built from scratch with one primary constraint: &lt;strong&gt;speed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yfsv1p6nqyi5o50f8rn.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7yfsv1p6nqyi5o50f8rn.gif" alt="We have to speed this up" width="640" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How MAGES Actually Runs Your Expression
&lt;/h2&gt;

&lt;p&gt;The core architectural decision that makes MAGES fast is that it doesn't walk an AST and evaluate it node-by-node at runtime (which is what YAMP effectively did, dressed up with reflection). Instead, every expression gets &lt;strong&gt;compiled down into a small set of VM instructions&lt;/strong&gt;, which are then executed sequentially by a lightweight stack-based virtual machine. Parse once, compile once, run the compact bytecode as many times as you want.&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff68fv6a7hjrts8yeveys.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%2Ff68fv6a7hjrts8yeveys.png" alt="MAGES pipeline: expression string parsed into an AST, compiled into VM instructions, executed on a stack-based VM to produce a result, with an alternate JS transpilation backend shown as a side path" width="800" height="245"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because the VM is just consuming a flat instruction stream, the interesting side effect is that the &lt;em&gt;front end&lt;/em&gt; (parser, AST) and &lt;em&gt;back end&lt;/em&gt; (what actually executes the instructions) are decoupled. Theoretically you could target something other than the built-in VM. I actually did this for fun once: I wrote a backend that "compiles" MAGES code into equivalent JavaScript and runs it on V8. It works, and it was a satisfying afternoon of yak-shaving. The more natural target would honestly be &lt;strong&gt;WebAssembly&lt;/strong&gt; - same idea, different, more useful destination - but that one's still sitting in the "would be cool" pile.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Compile once, reuse the compiled delegate as many times as you like&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;expOne&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"exp(1)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;expOne&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// 2.71828182845905&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Install it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Mages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interpret a one-off expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Mages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sin(2) * cos(pi / 4)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0.642970376623918&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pull a function back out and call it directly from C#:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"(x, y) =&amp;gt; x * y + 3 * sqrt(x)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;Mages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 18.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since v2.0, complex numbers are first-class citizens - this was a hard requirement to reach parity with what YAMP already offered, since a fair chunk of MAGES's real-world users do actual signal-processing-flavored math and were not going to be pleased with an engine that couldn't take a square root of &lt;code&gt;-1&lt;/code&gt; without complaining:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqrt(-1)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And since v3.0 (2024), things got a little playful: MAGES understands &lt;strong&gt;JSX&lt;/strong&gt;. Not "JSX-inspired" or "JSX-ish" - the actual JSX grammar, the same tags-with-embedded-expression-blocks syntax React (or Vue, or pretty much any modern web toolchain) uses. The tag structure, attributes, and children all follow the standard JSX rules you already know. The only twist is what happens &lt;em&gt;inside&lt;/em&gt; the curly-brace expression blocks: instead of that being JavaScript, it's MAGES doing the evaluating. Same shape, different engine running the logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"&amp;lt;div class={\"hello\" + \",\" + \"there\"}&amp;gt;&amp;lt;h1&amp;gt;Hi&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;World.&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt; | html"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// &amp;lt;div class="hello,there"&amp;gt;&amp;lt;h1&amp;gt;Hi&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;World.&amp;lt;/p&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;{"hello" + "," + "there"}&lt;/code&gt; there isn't JavaScript string concatenation - it's a MAGES expression, evaluated by the same stack-based VM as everything else, just sitting inside a JSX attribute the way you'd expect from any React component. Pipe the whole thing through &lt;code&gt;html&lt;/code&gt; and you get a serialized string back. It's a genuinely fun corner of the language: you get a templating syntax that looks completely familiar to anyone who's touched a modern frontend, backed by an evaluator that has nothing to do with a JS engine.&lt;/p&gt;

&lt;p&gt;Reflect on objects and functions at runtime via &lt;code&gt;type&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"((x, y, z) =&amp;gt; x + y + z) | type | json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { "name": "Function", "create": "[Function]",&lt;/span&gt;
&lt;span class="c1"&gt;//   "parameters": { "0": "x", "1": "y", "2": "z" } }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And use placeholders (&lt;code&gt;_&lt;/code&gt;) to curry arguments through the pipe operator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interpret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"
    var f = (x, y, z) =&amp;gt; x + 2 * y + 3 * z;
    5 | f(1, _, 2)
"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 17, computed as 1 + 2*5 + 3*2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pipe-and-placeholder combo is one of my favorite bits of syntax in the whole language - it reads almost like a small shell pipeline, but it's fully typed C# objects flowing through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small and dependency-free.&lt;/strong&gt; MAGES itself has zero runtime dependencies. It's a &lt;code&gt;netstandard2.1&lt;/code&gt; library, which means it happily runs on things like Unity or Mono, not just modern .NET.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fast by construction.&lt;/strong&gt; Compiling to a flat instruction stream for a stack-based VM means repeated evaluation is cheap - exactly what you want if you're evaluating the same handful of user-authored formulas thousands of times a second (looking at you, real-time math games and signal processing tools).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A genuinely pleasant expression syntax.&lt;/strong&gt; Closures, objects, lists, complex numbers, string interpolation, a pipe operator, and now JSX-flavored templating - it covers far more ground than "just arithmetic."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easy to embed.&lt;/strong&gt; &lt;code&gt;new Engine()&lt;/code&gt;, call &lt;code&gt;.Interpret()&lt;/code&gt; or &lt;code&gt;.Compile()&lt;/code&gt;, done. No hosting ceremony.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Great for domain-specific math.&lt;/strong&gt; If your app's users need to type in formulas - a measurement tool, a spreadsheet-like calculator, a scripting console - MAGES gives you that without writing your own parser (please don't write your own parser).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Struggles
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's a niche tool by nature.&lt;/strong&gt; It's not going to replace a general-purpose scripting language embedded in your app if you need full standard-library breadth, multi-file modules, or a package ecosystem. It's an expression/formula engine that grew some scripting-language features, not the other way around.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The (optional) type system doesn't exist yet.&lt;/strong&gt; More on this below, but right now everything is dynamically typed, and if you pass the wrong shape of data into a compiled expression, you find out at runtime like it's 2005.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tooling is thin.&lt;/strong&gt; There's no language server, no syntax highlighting extension, no step-through debugger. If you're building something where non-developers will be writing MAGES expressions, you're currently on your own for the authoring experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smaller community, smaller surface area for stuff to just already exist.&lt;/strong&gt; Compared to AngleSharp's few-hundred-million downloads, MAGES sits at a comparatively modest ~200K total NuGet downloads. That's not a criticism of the project so much as an honest expectation-setter: this is a focused tool for a specific job, not an ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqh3dvbwgbnsgw91vdrry.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqh3dvbwgbnsgw91vdrry.gif" alt="Small, but mighty" width="586" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Crew, Big Reach
&lt;/h2&gt;

&lt;p&gt;MAGES has never had a huge contributor list - it's a much more focused, much smaller codebase than something like AngleSharp. But it has picked up real sponsors along the way, credited right in the README: &lt;strong&gt;polytroper&lt;/strong&gt; (the studio behind that "SineRider, but the whole world is math" follow-up game I mentioned earlier), &lt;strong&gt;smapiot&lt;/strong&gt; (a company I previously worked for), and &lt;strong&gt;OMICRON Lab&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That last one deserves its own section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where MAGES Shows Up Today
&lt;/h2&gt;

&lt;p&gt;For a project with a comparatively small download count, MAGES keeps turning up in places that make me genuinely proud:&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F47kx6td3gwpxnbqe5ddv.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%2F47kx6td3gwpxnbqe5ddv.png" alt="MAGES sitting at the center, with Microsoft PowerToys, Flow Launcher, and OMICRON Lab measurement instruments as consumers above it, and LisaCore, embeddable scripting, and the SineRider game lineage below it" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/microsoft/PowerToys" rel="noopener noreferrer"&gt;Microsoft PowerToys&lt;/a&gt;&lt;/strong&gt; uses MAGES - it's listed directly in the project's own README as one of its notable consumers. Every time someone runs a calculation through a PowerToys utility, there's a decent chance MAGES is quietly doing the arithmetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Flow-Launcher/Flow.Launcher" rel="noopener noreferrer"&gt;Flow Launcher&lt;/a&gt;&lt;/strong&gt;, the 15K+ star Windows app launcher, depends on MAGES too - a natural fit, since "type an expression into a launcher and get an instant answer" is exactly the kind of thing MAGES was built for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OMICRON Lab&lt;/strong&gt;, an Austrian company building programmable test-and-measurement gear, uses MAGES as the expression engine behind their instruments. This is the one I'm most proud of: their hardware runs user-programmable math over live measurement data, which means MAGES had to be fast &lt;em&gt;and&lt;/em&gt; correct with complex numbers - no hand-waving allowed when someone's oscilloscope trace depends on getting the math right. This is also, not coincidentally, exactly why complex number support in v2.0 was a "must ship," not a "nice to have."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nuget.org/packages/LisaCore/" rel="noopener noreferrer"&gt;LisaCore&lt;/a&gt;&lt;/strong&gt; builds dynamic runtime code execution on top of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke37uwbq5nn2gf52b8wb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke37uwbq5nn2gf52b8wb.gif" alt="It's in more places than you'd expect" width="600" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of this was the plan when MAGES was a grant-funded rewrite of a reflection-based math parser. But "a small, fast, embeddable expression engine" turns out to be one of those quietly universal building blocks - much like AngleSharp's "a proper HTML parser for .NET" from the last post in this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next for MAGES
&lt;/h2&gt;

&lt;p&gt;A few directions I keep circling back to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real language tooling.&lt;/strong&gt; The dream is an actual &lt;strong&gt;Language Server Protocol (LSP)&lt;/strong&gt; implementation with a companion VS Code extension - proper syntax highlighting, inline diagnostics, maybe even autocomplete for the built-in function library. Right now, writing MAGES expressions means writing them blind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration into a new .NET web framework.&lt;/strong&gt; There are some interesting possibilities for using MAGES as a lightweight expression layer inside web tooling - think templated logic without pulling in a full scripting runtime. Nothing concrete to announce, but it's on the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional types.&lt;/strong&gt; This is the big one, and it's a lot harder than it looks. Adding &lt;em&gt;syntax&lt;/em&gt; for type annotations is the easy 10%. The actual hard part is making type checking meaningful - catching real errors before runtime, without turning simple one-line formulas into a TypeScript-style compiler project, and producing error messages that are actually useful rather than a wall of "type mismatch at position 47." I don't have a timeline for this. I have opinions about it, which is a different thing entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumsekiasavpz1w35q8lj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fumsekiasavpz1w35q8lj.gif" alt="Everything's fine" width="400" height="288"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;That's the MAGES story - from a reflection-powered math experiment riding along in a Unity sledding game, to a Thiel-grant-funded rewrite, to quietly doing arithmetic inside Microsoft PowerToys and Austrian test equipment. If you want to poke around, the code lives on &lt;a href="https://github.com/FlorianRappl/Mages" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and the package is on &lt;a href="https://www.nuget.org/packages/Mages/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next up in this series: another project, another origin story. Stay tuned.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>csharp</category>
      <category>compiling</category>
    </item>
    <item>
      <title>My OSS Projects: AngleSharp</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 09 Sep 2026 13:30:00 +0000</pubDate>
      <link>https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j</link>
      <guid>https://dev.to/florianrappl/my-oss-projects-anglesharp-3b8j</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is the first post in a new series, &lt;strong&gt;"My Open-Source Projects"&lt;/strong&gt;, where I go through some of the OSS projects I started or maintain and tell you the story behind them - the good, the bad, and the "why did I think this was a good idea at 30,000 feet" parts. First up: &lt;strong&gt;AngleSharp&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is AngleSharp, Actually?
&lt;/h2&gt;

&lt;p&gt;Before the story, the pitch: &lt;a href="https://github.com/AngleSharp/AngleSharp" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp&lt;/strong&gt;&lt;/a&gt; is a .NET library that parses HTML, SVG, MathML, CSS, and (to a good extent) XML, and gives you back a fully-featured, spec-conformant DOM. Not "DOM-ish." Not "good enough for scraping." The actual W3C DOM API, the same &lt;code&gt;querySelector&lt;/code&gt;, &lt;code&gt;querySelectorAll&lt;/code&gt;, and element interfaces you already know from the browser console - except it's C#, it runs headless, and there's no browser in sight.&lt;/p&gt;

&lt;p&gt;If you've ever reached for a regex to parse HTML: please don't. You know why. AngleSharp exists so that nobody has to write &lt;code&gt;&amp;lt;div[^&amp;gt;]*&amp;gt;&lt;/code&gt; ever again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstl9ykpvhne4kbr3f3ku.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstl9ykpvhne4kbr3f3ku.gif" alt="What could go wrong?" width="320" height="180"&gt;&lt;/a&gt;&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flqzgrrk0f5sel3xqewq6.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%2Flqzgrrk0f5sel3xqewq6.png" alt="AngleSharp parsing pipeline: raw markup to tokenizer to tree construction to DOM, then out to scraping, testing, or rendering" width="799" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, onto how it got here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Original (Slightly Unhinged) Idea
&lt;/h2&gt;

&lt;p&gt;Once upon a time, I had what I thought was a brilliant idea: build a cross-platform GUI toolkit using HTML, CSS, and C#. I was certainly not the first person to think of this - plenty of people had similar thoughts before me - but back then it was still a fairly fresh take.&lt;/p&gt;

&lt;p&gt;The more I thought it through, though, the more obvious it became: you can't just take HTML and CSS and strip them down to "the easy 80%". If you want this to actually work, you need the real deal - a proper HTML parser, a proper CSS engine, the whole circus. No shortcuts.&lt;/p&gt;

&lt;p&gt;So naturally, the reasonable thing to do was to just... write an HTML5 parser. From scratch. In C#. Sure, why not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coding at 30,000 Feet
&lt;/h2&gt;

&lt;p&gt;In 2013 I was flying to the Microsoft MVP Summit, and for reasons that made complete sense to me at the time, I brought a printed copy of the HTML5 spec with me. Not a novel. Not a magazine. The spec.&lt;/p&gt;

&lt;p&gt;I started coding on the plane. By the time we landed, the parser was already "working" - in the sense that the happy paths worked. Feed it well-formed markup and it would dutifully produce a DOM. Great success, right?&lt;/p&gt;

&lt;p&gt;Well, that's roughly the point where I discovered the dirty little secret of the HTML5 spec: it's massive not because HTML is a complicated language, but because of everything around it - error handling, edge cases, and the glorious pile of "what should happen when the markup is garbage" scenarios that browsers have been quietly agreeing on for decades. Turns out the "happy path" is maybe 20% of the actual spec. The other 80% is browsers being incredibly forgiving about human mistakes.&lt;/p&gt;

&lt;p&gt;I kept grinding away at it, and eventually most of the edge cases and tests turned green. And then, like clockwork, I hit the point every side project hits: frustration, a pause, and a "why am I doing this to myself" moment. Every maintainer knows this phase. It has no official name, but "why did I do this to myself" captures it well enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ship It Before It Goes Nowhere
&lt;/h2&gt;

&lt;p&gt;Eventually I picked it back up with a very simple thought: before this thing quietly dies in a local folder, let's write a CodeProject article about it and push the code to GitHub.&lt;/p&gt;

&lt;p&gt;I already had some open-source experience at that point - mostly as a contributor. The one project I actually maintained was &lt;a href="https://github.com/FlorianRappl/YAMP" rel="noopener noreferrer"&gt;YAMP&lt;/a&gt;, and thanks to some very patient people with a lot more GitHub experience than me, I'd picked up a few things about "doing OSS properly."&lt;/p&gt;

&lt;p&gt;So I published the article, pushed the repo, and went to bed.&lt;/p&gt;

&lt;p&gt;When I woke up, something had clearly happened. The repository already had 120+ stars, and the article was ranking surprisingly well. Nothing makes you suddenly very motivated about a side project like waking up to numbers you didn't expect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfln8mh9t2lyhhxow2tj.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfln8mh9t2lyhhxow2tj.gif" alt="Quite surprising, right" width="470" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I kept going. And going.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stabilizing, Then Breaking Everything (On Purpose)
&lt;/h2&gt;

&lt;p&gt;For a while, most of the effort went into the internals - making the parsing actually correct, not just "correct enough." Once that settled down, the API itself went through some pretty massive changes. Not always changes that users loved, if I'm honest. Breaking changes are never fun to be on the receiving end of, and I was on the giving end a fair bit during this period.&lt;/p&gt;

&lt;p&gt;The big turning point was &lt;strong&gt;AngleSharp 0.10&lt;/strong&gt;. This release quietly laid the groundwork for what would define AngleSharp going forward: modularity. Suddenly you could just add another library and your document could parse JavaScript. Add another one and you got CSS. This came from a lightweight-but-surprisingly-capable dependency injection system and a service-based configuration approach that let you chain extensions together like Lego bricks.&lt;/p&gt;

&lt;p&gt;That modular shape is still exactly how the ecosystem looks today:&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95khb9xduguddgr8n7kp.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%2F95khb9xduguddgr8n7kp.png" alt="AngleSharp core with satellite libraries: Css, Js, Xml, Io, Diffing, and Wasm" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Long Road to 1.0
&lt;/h2&gt;

&lt;p&gt;For years, I genuinely wasn't sure I could call the API "stable" and commit to semver. Not because it was shaky - it just felt like a big commitment. Eventually, after years of the API being &lt;em&gt;de facto&lt;/em&gt; stable (even if I hadn't officially blessed it as such), I released &lt;strong&gt;1.0&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From that point on, my main driver hasn't been "let's avoid a 2.0 or 3.0 forever." I have nothing against major version bumps. My actual rule is simpler: a breaking change needs to earn its place. If it's not genuinely useful enough to justify the pain it causes downstream, it doesn't happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Was Never a Solo Project
&lt;/h2&gt;

&lt;p&gt;I've told this story mostly in first person so far, which is a bit unfair, because AngleSharp would have died somewhere around "some happy paths seemed to work" if it had stayed a one-person effort.&lt;/p&gt;

&lt;p&gt;The project's &lt;code&gt;CONTRIBUTORS.md&lt;/code&gt; reads like a highlight reel of people who showed up over the years to fix a parser edge case, argue with me about API naming (we've had &lt;em&gt;opinions&lt;/em&gt;), or add a feature I didn't know I needed. In order of first contribution: Andreas Augustin, Jerrie Pelser, Liwen Guo, Raphael Ducom, Georgii Dolzhykov, Joel Verhagen, Michael Ganss, Robin Sue, Henry Roeland, Adrian Phinney, Andreas Håkansson, Jeremy Meng, Yehudah Asher, Dennis Daume, Jakub Świętek, Dennis Gorelik, Georgios Diamantopoulos, Brian Ricketts, Laurynas Ruškys, Martin Wakley, Bastian Buchholz, Keith Hall, Nikita Ilinykh, Thomas Bolon, Alexander Ubillus, Max Katz - and plenty more across the ecosystem repos who deserve just as much credit.&lt;/p&gt;

&lt;p&gt;One name on that list deserves a special callout: &lt;strong&gt;Michael Ganss&lt;/strong&gt;. He contributed to AngleSharp early on, then went off and built &lt;a href="https://github.com/mganss/HtmlSanitizer" rel="noopener noreferrer"&gt;&lt;strong&gt;HtmlSanitizer&lt;/strong&gt;&lt;/a&gt; - a library that cleans HTML of XSS payloads - directly on top of it. So somewhere out there, a chunk of the web's HTML sanitization is running through code that started as a side project on an airplane, maintained partly by someone who used to fix that airplane project's bugs. I don't think you can plan a story arc like that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvw1pk10v629qq4z1w1d.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwvw1pk10v629qq4z1w1d.gif" alt="Student becomes a master" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also worth saying clearly: AngleSharp is a &lt;a href="https://dotnetfoundation.org/" rel="noopener noreferrer"&gt;.NET Foundation&lt;/a&gt; project. That's not nothing - it means the project has a support structure behind it beyond just "one person's spare time and mild sleep deprivation."&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AngleSharp Shows Up Today
&lt;/h2&gt;

&lt;p&gt;It's easy to undersell this part, so let me just put some numbers on the table. As of writing, the core &lt;a href="https://www.nuget.org/packages/AngleSharp/" rel="noopener noreferrer"&gt;AngleSharp NuGet package&lt;/a&gt; alone sits at &lt;strong&gt;over 310 million downloads&lt;/strong&gt;. Add the satellite packages and you're comfortably north of 500 million installs across the ecosystem.&lt;/p&gt;

&lt;p&gt;The curve is the fun part - this is basically a hockey stick with a decade-long windup:&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/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkab7dwv84ijoc3y2nwfn.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%2Fkab7dwv84ijoc3y2nwfn.png" alt="NuGet download growth for AngleSharp, AngleSharp.Css, and AngleSharp.Diffing from 2013 to 2026, showing near-flat early years followed by steep exponential growth after 2019" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Barely a blip until around 2019, and then it just... doesn't stop. That little dip in early 2025 on the core package is a NuGet stats quirk, not a mass exodus - don't worry, nobody suddenly stopped parsing HTML.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcxutht4zfywnjrqmsxd.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffcxutht4zfywnjrqmsxd.gif" alt="Nice hockey stick" width="480" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few of the places AngleSharp quietly does the heavy lifting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nuget.org/packages/HtmlSanitizer/" rel="noopener noreferrer"&gt;HtmlSanitizer&lt;/a&gt;&lt;/strong&gt; (128M+ downloads on its own) uses AngleSharp to parse and re-render HTML while stripping out anything that could be used for cross-site scripting. If a .NET app on the internet safely accepts user-submitted HTML, there's a decent chance AngleSharp is standing guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://bunit.dev/" rel="noopener noreferrer"&gt;bUnit&lt;/a&gt;&lt;/strong&gt;, the go-to unit testing library for Blazor components, uses AngleSharp under the hood (via &lt;code&gt;AngleSharp.Diffing&lt;/code&gt;) for its semantic HTML comparisons - the thing that lets you assert "this rendered markup is equivalent to that markup" without caring about attribute order or whitespace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nuget.org/packages/PreMailer.Net/" rel="noopener noreferrer"&gt;PreMailer.Net&lt;/a&gt;&lt;/strong&gt; inlines CSS into HTML emails (because email clients are still living in 2003) using AngleSharp to actually understand the markup and styles it's rewriting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nuget.org/packages/WebDriverManager/" rel="noopener noreferrer"&gt;WebDriverManager&lt;/a&gt;&lt;/strong&gt; for .NET, which manages Selenium WebDriver binaries, also leans on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then there's the list of projects that depend on it directly - some of which you've almost certainly used: &lt;strong&gt;Bitwarden&lt;/strong&gt; (the password manager), &lt;strong&gt;Jackett&lt;/strong&gt; and &lt;strong&gt;Prowlarr&lt;/strong&gt; (torrent/indexer tooling), &lt;strong&gt;Playnite&lt;/strong&gt; (game library manager), &lt;strong&gt;ArchiSteamFarm&lt;/strong&gt;, &lt;strong&gt;OrchardCore&lt;/strong&gt; (a full CMS/app framework), &lt;strong&gt;OpenIddict&lt;/strong&gt; (OAuth2/OpenID Connect for .NET), &lt;strong&gt;YoutubeExplode&lt;/strong&gt;, and even Microsoft's own &lt;a href="https://github.com/dotnet/AspNetCore.Docs" rel="noopener noreferrer"&gt;&lt;strong&gt;ASP.NET Core documentation tooling&lt;/strong&gt;&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;None of this was the plan when I was scribbling parser states on a plane. But it turns out "a really solid, standards-compliant HTML parser for .NET" is one of those unglamorous building blocks that ends up everywhere once it exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Install the core package from NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package AngleSharp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And parse your first document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;AngleSharp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;AngleSharp.Html.Parser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HtmlParser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ParseDocumentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;h1&amp;gt;Hello!&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;AngleSharp says hi.&amp;lt;/p&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;heading&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QuerySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"h1"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TextContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Hello!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the "hello world" version. Most real usage, though, goes through a &lt;code&gt;BrowsingContext&lt;/code&gt;, which acts like a browser tab - it can load documents from the network, follow links, submit forms, and generally behave like something a bit more alive than a static parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;AngleSharp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDefaultLoader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BrowsingContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QuerySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"a"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"href"&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;Want CSS support too? Add the plugin and register it in the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package AngleSharp.Css
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDefaultLoader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCss&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BrowsingContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://example.com"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QuerySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ComputeCurrentStyle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPropertyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"background-color"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same idea for scripting (&lt;code&gt;AngleSharp.Js&lt;/code&gt;), XML validation (&lt;code&gt;AngleSharp.Xml&lt;/code&gt;), or semantic diffing of HTML fragments (&lt;code&gt;AngleSharp.Diffing&lt;/code&gt;) - one &lt;code&gt;With...()&lt;/code&gt; call, and a new capability shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Shines
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Standards over vibes.&lt;/strong&gt; The parser follows the actual HTML5 parsing algorithm, including its error-recovery rules. Feed it broken markup, and it recovers the way a browser would - not the way a regex-and-hope solution would.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real DOM, not a wrapper.&lt;/strong&gt; &lt;code&gt;querySelector&lt;/code&gt;, &lt;code&gt;querySelectorAll&lt;/code&gt;, LINQ-to-DOM, form submission, navigation via &lt;code&gt;BrowsingContext&lt;/code&gt; - it feels like scripting a browser tab from C#, minus the browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular by design.&lt;/strong&gt; Only pay (in dependencies and startup cost) for what you actually use. Need CSS cascade computation? Add one package. Need to run inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags? Add another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance.&lt;/strong&gt; It's not trying to be a browser engine, and that's exactly why it's fast - large documents parse in milliseconds, with internal reuse of elements to keep allocations down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It composes well with automated testing.&lt;/strong&gt; &lt;code&gt;AngleSharp.Diffing&lt;/code&gt; in particular is great for asserting "this HTML is semantically the same as that HTML" without caring about attribute order or whitespace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where It Struggles
&lt;/h2&gt;

&lt;p&gt;No project is without trade-offs, and I'd rather tell you about them than let you find out the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not a browser.&lt;/strong&gt; There's no layout engine, no painting, no actual visual rendering (yet - more on that below). If you need pixel-accurate rendering, you still want something like Playwright or a headless Chromium.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript support is limited.&lt;/strong&gt; &lt;code&gt;AngleSharp.Js&lt;/code&gt; runs on &lt;a href="https://github.com/sebastienros/jint" rel="noopener noreferrer"&gt;Jint&lt;/a&gt;, which is a real ECMAScript engine, but it's not V8. Heavy, modern JS-driven pages (SPAs relying on a full browser runtime) are not its comfort zone, but definitely possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ecosystem is spread across several repositories.&lt;/strong&gt; Core, Css, Js, Xml, Io, Diffing, Wasm - it's modular by design, which is a strength, but it also means version compatibility between packages occasionally needs a bit of attention (dependency ranges across separately-versioned repos are always a &lt;em&gt;fun&lt;/em&gt; problem to own - but generally the only dependency they all have is the core lib).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Breaking changes did happen historically.&lt;/strong&gt; Pre-1.0 AngleSharp went through real API churn. It's stable and semver-respecting now, but if you find decade-old blog posts referencing the API, expect it to look different today.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Wider Ecosystem
&lt;/h2&gt;

&lt;p&gt;Depending on what you need, the packages you'll likely run into are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp&lt;/strong&gt;&lt;/a&gt; (&lt;a href="https://www.nuget.org/packages/AngleSharp/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;) - the core: HTML5/XML parsing, DOM, configuration.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp.Css" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp.Css&lt;/strong&gt;&lt;/a&gt; - CSSOM, selectors, cascades, computed styles.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp.Js" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp.Js&lt;/strong&gt;&lt;/a&gt; - JavaScript execution via Jint.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp.Xml" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp.Xml&lt;/strong&gt;&lt;/a&gt; - XML parsing plus XSD/DTD validation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp.Io" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp.Io&lt;/strong&gt;&lt;/a&gt; - requesters, cookies, and the networking stack.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp.Diffing" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp.Diffing&lt;/strong&gt;&lt;/a&gt; - semantic DOM diffing, great for tests.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/AngleSharp/AngleSharp.Wasm" rel="noopener noreferrer"&gt;&lt;strong&gt;AngleSharp.Wasm&lt;/strong&gt;&lt;/a&gt; - running AngleSharp inside the browser via WebAssembly/Blazor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to talk to actual humans about it best case is to open an issue in one of the repos. Historically, there's also a &lt;a href="https://gitter.im/AngleSharp/AngleSharp" rel="noopener noreferrer"&gt;Gitter chat&lt;/a&gt; and a tag that was / is used on &lt;a href="https://stackoverflow.com/tags/anglesharp" rel="noopener noreferrer"&gt;Stack Overflow&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next: Coming Full Circle
&lt;/h2&gt;

&lt;p&gt;Remember that original goal - a cross-platform GUI built on HTML and CSS? That idea got shelved pretty early on in favor of "let's just get the fundamentals right first." But it never really left.&lt;/p&gt;

&lt;p&gt;The next big frontier for AngleSharp is &lt;strong&gt;rendering&lt;/strong&gt;. The plan is a general-purpose headless renderer that can be used interactively - moving a pointer, sending keystrokes, the works - or just to grab a screenshot. On top of that, the goal is a cross-platform control (most likely built on &lt;strong&gt;Avalonia&lt;/strong&gt;) that can be embedded into apps.&lt;/p&gt;

&lt;p&gt;Am I building a browser? Let's not get ahead of ourselves. But it's fair to say the project is closer to its original vision now than it's been at any point in the last decade.&lt;/p&gt;




&lt;p&gt;That's the AngleSharp story - from a stack of printed spec pages on an airplane to a mature parsing ecosystem with XSD validation and WASM support. If you want to poke around, the code lives on &lt;a href="https://github.com/AngleSharp/AngleSharp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, and the package is on &lt;a href="https://www.nuget.org/packages/AngleSharp/" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next up in this series: another one of my projects, and probably another origin story that started with me underestimating how much work something would be. Stay tuned!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>opensource</category>
      <category>csharp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>netpack</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Thu, 09 Jan 2025 23:51:37 +0000</pubDate>
      <link>https://dev.to/florianrappl/netpack-5ej4</link>
      <guid>https://dev.to/florianrappl/netpack-5ej4</guid>
      <description>&lt;p&gt;This will be a short post... but hopefully with a longer one following up.&lt;/p&gt;

&lt;p&gt;During my winter vacation I got some time to do one experiment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can web tooling created using C#/.NET yield comparable performance to Rust / Go / Zig ...?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I did some coding... (that you can &lt;a href="https://github.com/FlorianRappl/netpack" rel="noopener noreferrer"&gt;find on GitHub&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Process
&lt;/h2&gt;

&lt;p&gt;I started with a crude bundler logic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open files&lt;/li&gt;
&lt;li&gt;Read their content&lt;/li&gt;
&lt;li&gt;Use a regular expression to detect, e.g., import statements in JS files&lt;/li&gt;
&lt;li&gt;Resolve linked modules&lt;/li&gt;
&lt;li&gt;Open resolved / existing package.json files to identify module paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was easy: Using AoT (ahead-of-time compilation) .NET can certainly be used for highly performing web projects.&lt;/p&gt;

&lt;p&gt;So I continued a bit with the experiment; replacing regular expressions with actual code understanding.&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR; Results
&lt;/h2&gt;

&lt;p&gt;The answer is: yes! 🤯&lt;/p&gt;

&lt;p&gt;The bundler is at the moment feature-incomplete, but the first results are quite strong. The benchmarks shown in the &lt;a href="https://github.com/FlorianRappl/netpack" rel="noopener noreferrer"&gt;README&lt;/a&gt; indicate that the performance is definitely in the same ballpark as other tools. So fast enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  More Details
&lt;/h2&gt;

&lt;p&gt;Personally, I think that C#/.NET is much less complicated than Rust and more powerful than Go. It comes with some drawbacks as well - not gonna lie.&lt;/p&gt;

&lt;p&gt;The main reason why C#/.NET can be viable in that space is AoT. Without AoT the startup performance (as well as runtime requirements) is killing the whole idea.&lt;/p&gt;

&lt;p&gt;AoT, on the other hand, comes with some challenges. Some libraries cannot be used or require some work to be integrated. Hence, some of the flexibility of .NET cannot be used.&lt;/p&gt;

&lt;p&gt;For the largest test project also used by tools such as rspack we get these results:&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/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwc9g850i2pseizq9klpi.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.amazonaws.com%2Fuploads%2Farticles%2Fwc9g850i2pseizq9klpi.png" alt="Bundler performance" width="700" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that even the bundler is feature incomplete, it is crafted enough to produce a valid result on the project. So even though all results are at the moment preliminary there is at least some validity to it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test&lt;/th&gt;
&lt;th&gt;esbuild&lt;/th&gt;
&lt;th&gt;rspack&lt;/th&gt;
&lt;th&gt;Vite&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;netpack&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small lib&lt;/td&gt;
&lt;td&gt;326ms&lt;/td&gt;
&lt;td&gt;611ms&lt;/td&gt;
&lt;td&gt;601ms&lt;/td&gt;
&lt;td&gt;359ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small project&lt;/td&gt;
&lt;td&gt;670ms&lt;/td&gt;
&lt;td&gt;912ms&lt;/td&gt;
&lt;td&gt;1658ms&lt;/td&gt;
&lt;td&gt;418ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium project&lt;/td&gt;
&lt;td&gt;1931ms&lt;/td&gt;
&lt;td&gt;2877ms&lt;/td&gt;
&lt;td&gt;10601ms&lt;/td&gt;
&lt;td&gt;974ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large project&lt;/td&gt;
&lt;td&gt;2189ms&lt;/td&gt;
&lt;td&gt;2422ms&lt;/td&gt;
&lt;td&gt;13710ms&lt;/td&gt;
&lt;td&gt;1357ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So yes, netpack already beats the competition and even has potential for even better performance. While it can be optimized further, it will also loose some performance when things such as sourcemaps or tree shaking are introduced. Right now I am positive that in total it should be around the same as of now due to the potential optimizations (such as streaming in the JS AST generation).&lt;/p&gt;

&lt;p&gt;The biggest hurdle at the moment is that it only supports JS(X) - no TypeScript yet (it tries to parse these files, but once types are used it will fail). It would be "fairly" easy to support, but I would need to fork &lt;a href="https://github.com/adams85/acornima/" rel="noopener noreferrer"&gt;Acornima&lt;/a&gt; for that and that's something I'd only do if there is enough buzz around the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Outlook
&lt;/h2&gt;

&lt;p&gt;There are many more things that would be superb to get into this. Some basics need to be cleared first though. Things such as sourcemaps, TypeScript support, or maybe a configuration system would be great.&lt;/p&gt;

&lt;p&gt;There are some things in this experiment that no other bundler does. For instance, if your HTML entry point has an importmap, then the entries in the importmap are &lt;em&gt;automatically&lt;/em&gt; taken as externals. Likewise, you can set certain dependencies as shared - in this case there are automatically importmap entries / an importmap created in the resulting HTML. Quite neat.&lt;/p&gt;

&lt;p&gt;In the future the bundler will have native (i.e., out-of-the-box) support for SASS, CSS modules, CSS-in-JS, as well as module federation and native federation.&lt;/p&gt;

&lt;p&gt;What are your thoughts? Do you think this is a viable idea or just trash? Is a fast .NET-native bundler with sensible defaults something we need?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>dotnet</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Great edition - thanks to all the authors for their hard work!</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 04 Dec 2024 08:31:48 +0000</pubDate>
      <link>https://dev.to/florianrappl/great-edition-thanks-to-all-the-authors-for-their-hard-work-4mm5</link>
      <guid>https://dev.to/florianrappl/great-edition-thanks-to-all-the-authors-for-their-hard-work-4mm5</guid>
      <description>&lt;div class="ltag__link"&gt;
  &lt;a href="/florianrappl" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F268296%2Feb6a1ad0-17d3-4302-9c44-a263a666b072.jpg" alt="florianrappl"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/florianrappl/whats-hot-in-web-dev-42ng" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;What's Hot in Web Dev?&lt;/h2&gt;
      &lt;h3&gt;Florian Rappl ・ Dec 4&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#webdev&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#javascript&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#frontend&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#node&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


</description>
    </item>
    <item>
      <title>What's Hot in Web Dev?</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 04 Dec 2024 08:31:02 +0000</pubDate>
      <link>https://dev.to/florianrappl/whats-hot-in-web-dev-42ng</link>
      <guid>https://dev.to/florianrappl/whats-hot-in-web-dev-42ng</guid>
      <description>&lt;p&gt;This year I started a new series on LinkedIn - "Advanced Links for Frontend". Each issue has 10 links to outstanding posts / articles. This bundle contains the links from the last 5 issues (issue 66 to issue 70).&lt;/p&gt;

&lt;p&gt;I hope you enjoy this collection. Let me know in the comments which of these articles is your favorite (and why).&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 66
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How Bun supports V8 APIs without using V8 (part 2)&lt;/strong&gt; (&lt;a href="https://bun.sh/blog/how-bun-supports-v8-apis-without-using-v8-part-2" rel="noopener noreferrer"&gt;https://bun.sh/blog/how-bun-supports-v8-apis-without-using-v8-part-2&lt;/a&gt;) by Ben Grant&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the best React Native list component?&lt;/strong&gt; (&lt;a href="https://expo.dev/blog/what-is-the-best-react-native-list-component" rel="noopener noreferrer"&gt;https://expo.dev/blog/what-is-the-best-react-native-list-component&lt;/a&gt;) by Simon Grimm&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centering things: a solved problem?&lt;/strong&gt; (&lt;a href="https://fullystacked.net/centering-things/" rel="noopener noreferrer"&gt;https://fullystacked.net/centering-things/&lt;/a&gt;) by Ollie Williams&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebVM 2.0: A complete Linux Desktop Environment in the browser via WebAssembly&lt;/strong&gt; (&lt;a href="https://labs.leaningtech.com/blog/webvm-20" rel="noopener noreferrer"&gt;https://labs.leaningtech.com/blog/webvm-20&lt;/a&gt;) by Alessandro Pignotti&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I Followed the Official AWS Amplify Guide and was Charged $1,100&lt;/strong&gt; (&lt;a href="https://elliott-king.github.io/2024/10/amplify-overcharge/" rel="noopener noreferrer"&gt;https://elliott-king.github.io/2024/10/amplify-overcharge/&lt;/a&gt;) by Elliott King&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Different (and Modern) Ways to Toggle Content&lt;/strong&gt; (&lt;a href="https://css-tricks.com/the-different-and-modern-ways-to-toggle-content/" rel="noopener noreferrer"&gt;https://css-tricks.com/the-different-and-modern-ways-to-toggle-content/&lt;/a&gt;) by Daniel Schwarz&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Typescript in node.js scripts without actually writing Typescript&lt;/strong&gt; (&lt;a href="https://www.jonathancreamer.com/using-typescript-in-node-js-scripts-without-actually-writing-typescript/" rel="noopener noreferrer"&gt;https://www.jonathancreamer.com/using-typescript-in-node-js-scripts-without-actually-writing-typescript/&lt;/a&gt;) by Jonathan Creamer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overflow Clip&lt;/strong&gt; (&lt;a href="https://ishadeed.com/article/overflow-clip/" rel="noopener noreferrer"&gt;https://ishadeed.com/article/overflow-clip/&lt;/a&gt;) by Ahmad Shadeed&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional Props in React Using Type Discrimination&lt;/strong&gt; (&lt;a href="https://elanmed.dev/blog/conditional-props-using-type-discrimination" rel="noopener noreferrer"&gt;https://elanmed.dev/blog/conditional-props-using-type-discrimination&lt;/a&gt;) by Elan Medoff&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Micro Components - a different approach to a simpler component-based web&lt;/strong&gt; (&lt;a href="https://www.wearedevelopers.com/magazine/micro-components---an-alternative-to-web-components-and-frameworks/" rel="noopener noreferrer"&gt;https://www.wearedevelopers.com/magazine/micro-components---an-alternative-to-web-components-and-frameworks/&lt;/a&gt;) by Chris Heilmann&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 67
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JavaScript's ??= Operator: Default Values Made Simple&lt;/strong&gt; (&lt;a href="https://www.trevorlasn.com/blog/javascript-nullish-coalescing-assignment-operator" rel="noopener noreferrer"&gt;https://www.trevorlasn.com/blog/javascript-nullish-coalescing-assignment-operator&lt;/a&gt;) by Trevor Lasn&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fit-to-Width Text: A New Technique&lt;/strong&gt; (&lt;a href="https://kizu.dev/fit-to-width/" rel="noopener noreferrer"&gt;https://kizu.dev/fit-to-width/&lt;/a&gt;) by Roman Komarov&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-contained Executable Programs with Deno Compile&lt;/strong&gt; (&lt;a href="https://deno.com/blog/deno-compile-executable-programs" rel="noopener noreferrer"&gt;https://deno.com/blog/deno-compile-executable-programs&lt;/a&gt;) by Ryan Dahl and Andy Jiang&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New in React 19 - Pre-warming&lt;/strong&gt; (&lt;a href="https://github.com/facebook/react/issues/29898" rel="noopener noreferrer"&gt;https://github.com/facebook/react/issues/29898&lt;/a&gt;) by Ricky Hanlon&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will we care about frameworks in the future?&lt;/strong&gt; (&lt;a href="https://paul.kinlan.me/will-we-care-about-frameworks-in-the-future/" rel="noopener noreferrer"&gt;https://paul.kinlan.me/will-we-care-about-frameworks-in-the-future/&lt;/a&gt;) by Paul Kinlan&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introducing the vlt Package Manager &amp;amp; Serverless Registry&lt;/strong&gt; (&lt;a href="https://blog.vlt.sh/blog/introducing-vlt-and-vsr" rel="noopener noreferrer"&gt;https://blog.vlt.sh/blog/introducing-vlt-and-vsr&lt;/a&gt;) by Darcy Clark et al.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Saga Pattern in Elixir&lt;/strong&gt; (&lt;a href="https://peterullrich.com/saga-pattern-in-elixir" rel="noopener noreferrer"&gt;https://peterullrich.com/saga-pattern-in-elixir&lt;/a&gt;) by Peter Ullrich&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating Random Mazes with JavaScript&lt;/strong&gt; (&lt;a href="https://cloudfour.com/thinks/generating-random-mazes-with-javascript/" rel="noopener noreferrer"&gt;https://cloudfour.com/thinks/generating-random-mazes-with-javascript/&lt;/a&gt;) by Paul Hebert&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Beautiful focus outlines&lt;/strong&gt; (&lt;a href="https://medienbaecker.com/articles/focus-outlines" rel="noopener noreferrer"&gt;https://medienbaecker.com/articles/focus-outlines&lt;/a&gt;) by Thomas Günther&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Tragedy of Running an Old Node Project&lt;/strong&gt; (&lt;a href="https://abdisalan.com/posts/tragedy-running-old-node-project" rel="noopener noreferrer"&gt;https://abdisalan.com/posts/tragedy-running-old-node-project&lt;/a&gt;) by Abdisalan Mohamud&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 68
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Long Frames and INP: Understanding Post-Load Performance&lt;/strong&gt; (&lt;a href="https://dev.to/viniciusdallacqua/long-frames-and-inp-understanding-post-load-performance-2maa"&gt;https://dev.to/viniciusdallacqua/long-frames-and-inp-understanding-post-load-performance-2maa&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/viniciusdallacqua"&gt;@viniciusdallacqua&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reporting Core Web Vitals With The Performance API&lt;/strong&gt; (&lt;a href="https://www.smashingmagazine.com/2024/02/reporting-core-web-vitals-performance-api/" rel="noopener noreferrer"&gt;https://www.smashingmagazine.com/2024/02/reporting-core-web-vitals-performance-api/&lt;/a&gt;) by Geoff Graham&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;React Router v7&lt;/strong&gt; (&lt;a href="https://remix.run/blog/react-router-v7" rel="noopener noreferrer"&gt;https://remix.run/blog/react-router-v7&lt;/a&gt;) by Michael Jackson&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Shadow DOM is in the front&lt;/strong&gt; (&lt;a href="https://abstract.properties/the-shadow-dom-is-in-the-front.html" rel="noopener noreferrer"&gt;https://abstract.properties/the-shadow-dom-is-in-the-front.html&lt;/a&gt;) by Sufian Rhazi&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind CSS v4.0 Beta 1&lt;/strong&gt; (&lt;a href="https://tailwindcss.com/blog/tailwindcss-v4-beta" rel="noopener noreferrer"&gt;https://tailwindcss.com/blog/tailwindcss-v4-beta&lt;/a&gt;) by Adam Wathan&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Announcing TypeScript 5.7&lt;/strong&gt; (&lt;a href="https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/" rel="noopener noreferrer"&gt;https://devblogs.microsoft.com/typescript/announcing-typescript-5-7/&lt;/a&gt;) by Daniel Rosenwasser&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling Cookies is a Minefield&lt;/strong&gt; (&lt;a href="https://grayduck.mn/2024/11/21/handling-cookies-is-a-minefield/" rel="noopener noreferrer"&gt;https://grayduck.mn/2024/11/21/handling-cookies-is-a-minefield/&lt;/a&gt;) by April King&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This Website is Hosted on Bluesky&lt;/strong&gt; (&lt;a href="https://danielmangum.com/posts/this-website-is-hosted-on-bluesky/" rel="noopener noreferrer"&gt;https://danielmangum.com/posts/this-website-is-hosted-on-bluesky/&lt;/a&gt;) by Daniel Mangum&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero Disk Architecture&lt;/strong&gt; (&lt;a href="https://avi.im/blag/2024/zero-disk-architecture/" rel="noopener noreferrer"&gt;https://avi.im/blag/2024/zero-disk-architecture/&lt;/a&gt;) by Avinash Sajjanshetty&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google’s Chrome Worth Up to $20 Billion If Judge Orders Sale&lt;/strong&gt; (&lt;a href="https://www.bloomberg.com/news/articles/2024-11-18/doj-will-push-google-to-sell-off-chrome-to-break-search-monopoly" rel="noopener noreferrer"&gt;https://www.bloomberg.com/news/articles/2024-11-18/doj-will-push-google-to-sell-off-chrome-to-break-search-monopoly&lt;/a&gt;) by Leah Nylen and Josh Sisco&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 69
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Webflow Design Language: Bridging The Gap Between Design &amp;amp; Code&lt;/strong&gt; (&lt;a href="https://webflow.com/blog/webflow-design-language" rel="noopener noreferrer"&gt;https://webflow.com/blog/webflow-design-language&lt;/a&gt;) by Merrick Christensen&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building Reddit’s Frontend with Vite&lt;/strong&gt; (&lt;a href="https://www.reddit.com/r/RedditEng/comments/1dhztk8/building_reddits_frontend_with_vite/" rel="noopener noreferrer"&gt;https://www.reddit.com/r/RedditEng/comments/1dhztk8/building_reddits_frontend_with_vite/&lt;/a&gt;) by Jim Simon&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interop and Hard Problems&lt;/strong&gt; (&lt;a href="https://bkardell.com/blog/debt.html" rel="noopener noreferrer"&gt;https://bkardell.com/blog/debt.html&lt;/a&gt;) by Brian Kardell&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vite 6.0 is out!&lt;/strong&gt; (&lt;a href="https://vite.dev/blog/announcing-vite6.html" rel="noopener noreferrer"&gt;https://vite.dev/blog/announcing-vite6.html&lt;/a&gt;) by Evan You&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Complete Guide To Live Validation UX&lt;/strong&gt; (&lt;a href="https://www.smashingmagazine.com/2022/09/inline-validation-web-forms-ux/" rel="noopener noreferrer"&gt;https://www.smashingmagazine.com/2022/09/inline-validation-web-forms-ux/&lt;/a&gt;) by Vitaly Friedman&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Placeholders in Form Fields Are Harmful&lt;/strong&gt; (&lt;a href="https://www.nngroup.com/articles/form-design-placeholders/" rel="noopener noreferrer"&gt;https://www.nngroup.com/articles/form-design-placeholders/&lt;/a&gt;) by Katie Sherwin&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser APIs: Must-Know Secrets for Every Frontend Developer!&lt;/strong&gt; (&lt;a href="https://dev.to/mukhilpadmanabhan/browser-apis-must-know-secrets-for-every-frontend-developer-55lk"&gt;https://dev.to/mukhilpadmanabhan/browser-apis-must-know-secrets-for-every-frontend-developer-55lk&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/mukhilpadmanabhan"&gt;@mukhilpadmanabhan&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I learned React Native as a web developer, and I got everything wrong&lt;/strong&gt; (&lt;a href="https://fernandorojo.co/mistakes" rel="noopener noreferrer"&gt;https://fernandorojo.co/mistakes&lt;/a&gt;) by Fernando Rojo&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why TanStack Start is Ditching Adapters&lt;/strong&gt; (&lt;a href="https://tanstack.com/blog/why-tanstack-start-is-ditching-adapters" rel="noopener noreferrer"&gt;https://tanstack.com/blog/why-tanstack-start-is-ditching-adapters&lt;/a&gt;) by Tanner Linsley&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vitest vs. Jest&lt;/strong&gt; (&lt;a href="https://www.speakeasy.com/post/vitest-vs-jest" rel="noopener noreferrer"&gt;https://www.speakeasy.com/post/vitest-vs-jest&lt;/a&gt;) by Nolan Sullivan&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 70
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why am I writing a JavaScript toolchain in Zig?&lt;/strong&gt; (&lt;a href="https://injuly.in/blog/announcing-jam/index.html" rel="noopener noreferrer"&gt;https://injuly.in/blog/announcing-jam/index.html&lt;/a&gt;) by Srijan Paul&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;November 20, 2024 Release&lt;/strong&gt; (&lt;a href="https://react-spectrum.adobe.com/releases/2024-11-20.html" rel="noopener noreferrer"&gt;https://react-spectrum.adobe.com/releases/2024-11-20.html&lt;/a&gt;) by React Aria&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Effortless UI Spring Animations: A Two-Parameter Approach&lt;/strong&gt; (&lt;a href="https://www.kvin.me/posts/effortless-ui-spring-animations" rel="noopener noreferrer"&gt;https://www.kvin.me/posts/effortless-ui-spring-animations&lt;/a&gt;) by Kevin Grajeda&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If Not React, Then What?&lt;/strong&gt; (&lt;a href="https://infrequently.org/2024/11/if-not-react-then-what/" rel="noopener noreferrer"&gt;https://infrequently.org/2024/11/if-not-react-then-what/&lt;/a&gt;) by Alex Russell&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparing AWS S3 with Cloudflare R2: Price, Performance and User Experience&lt;/strong&gt; (&lt;a href="https://kerkour.com/aws-s3-vs-cloudflare-r2-price-performance-user-experience" rel="noopener noreferrer"&gt;https://kerkour.com/aws-s3-vs-cloudflare-r2-price-performance-user-experience&lt;/a&gt;) by Sylvain Kerkour&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Must-Have Obsidian plugins for 2025&lt;/strong&gt; (&lt;a href="https://www.dsebastien.net/2022-10-19-the-must-have-obsidian-plugins/" rel="noopener noreferrer"&gt;https://www.dsebastien.net/2022-10-19-the-must-have-obsidian-plugins/&lt;/a&gt;) by Sebastien Dubois&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Layered Approach to Speculation Rules&lt;/strong&gt; (&lt;a href="https://csswizardry.com/2024/12/a-layered-approach-to-speculation-rules/" rel="noopener noreferrer"&gt;https://csswizardry.com/2024/12/a-layered-approach-to-speculation-rules/&lt;/a&gt;) by Harry Roberts&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing Is Something&lt;/strong&gt; (&lt;a href="https://blog.jim-nielsen.com/2024/nothing-is-something/" rel="noopener noreferrer"&gt;https://blog.jim-nielsen.com/2024/nothing-is-something/&lt;/a&gt;) by Jim Nielsen&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Increasing Vite's potential with the Environment API&lt;/strong&gt; (&lt;a href="https://green.sapphi.red/blog/increasing-vites-potential-with-the-environment-api" rel="noopener noreferrer"&gt;https://green.sapphi.red/blog/increasing-vites-potential-with-the-environment-api&lt;/a&gt;) by sapphi-red&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existential React questions and a perfect Modal Dialog&lt;/strong&gt; (&lt;a href="https://www.developerway.com/posts/hard-react-questions-and-modal-dialog" rel="noopener noreferrer"&gt;https://www.developerway.com/posts/hard-react-questions-and-modal-dialog&lt;/a&gt;) by Nadia Makarevich&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These are all outstanding articles by masterful authors. I enjoyed reading them all - I hope you did find something in there, too.&lt;/p&gt;

&lt;p&gt;👉 Follow me on &lt;a href="https://www.linkedin.com/in/florian-rappl/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/FlorianRappl" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, &lt;a href="https://bsky.app/profile/florianrappl.bsky.social" rel="noopener noreferrer"&gt;BlueSky&lt;/a&gt;, or here for more to come.&lt;/p&gt;

&lt;p&gt;🙏 Thanks to all the authors and contributors for their hard work!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>node</category>
    </item>
    <item>
      <title>🤯 50 Articles to Become a Web Dev Guru</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Fri, 15 Nov 2024 19:15:31 +0000</pubDate>
      <link>https://dev.to/florianrappl/50-articles-to-become-a-web-dev-guru-71e</link>
      <guid>https://dev.to/florianrappl/50-articles-to-become-a-web-dev-guru-71e</guid>
      <description>&lt;p&gt;This year I started a new series on LinkedIn - "Advanced Links for Frontend". Each issue has 10 links to outstanding posts / articles. This bundle contains the links from the last 5 issues (issue 61 to issue 65).&lt;/p&gt;

&lt;p&gt;I hope you enjoy this collection. Let me know in the comments which of these articles is your favorite (and why).&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 61
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Liskov’s Gun: The parallel evolution of React and Web Components&lt;/strong&gt; (&lt;a href="https://www.baldurbjarnason.com/2024/liskovs-gun/" rel="noopener noreferrer"&gt;https://www.baldurbjarnason.com/2024/liskovs-gun/&lt;/a&gt;) by Baldur Bjarnason&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mozilla rebrand takes from early internet aesthetics, to move its cause to reclaim the web&lt;/strong&gt; (&lt;a href="https://www.itsnicethat.com/articles/jkr-mozilla-rebrand-graphic-design-project-240924" rel="noopener noreferrer"&gt;https://www.itsnicethat.com/articles/jkr-mozilla-rebrand-graphic-design-project-240924&lt;/a&gt;) by Ellis Tree&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's New in Ruby on Rails 8&lt;/strong&gt; (&lt;a href="https://blog.appsignal.com/2024/10/07/whats-new-in-ruby-on-rails-8.html" rel="noopener noreferrer"&gt;https://blog.appsignal.com/2024/10/07/whats-new-in-ruby-on-rails-8.html&lt;/a&gt;) by Damilola Olantuji&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A modest critique of Htmx&lt;/strong&gt; (&lt;a href="https://chrisdone.com/posts/htmx-critique/" rel="noopener noreferrer"&gt;https://chrisdone.com/posts/htmx-critique/&lt;/a&gt;) by Chris Done&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML is for people&lt;/strong&gt; (&lt;a href="https://htmlforpeople.com/" rel="noopener noreferrer"&gt;https://htmlforpeople.com/&lt;/a&gt;) by Blake Watson&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;17 Must-know React Projects for Developers 👩‍💻 🔥&lt;/strong&gt; (&lt;a href="https://dev.to/tolgee_i18n/17-must-know-react-projects-for-developers-28a2"&gt;https://dev.to/tolgee_i18n/17-must-know-react-projects-for-developers-28a2&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/anmolbaranwal"&gt;@anmolbaranwal&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The story of web framework Hono, from the creator of Hono&lt;/strong&gt; (&lt;a href="https://blog.cloudflare.com/the-story-of-web-framework-hono-from-the-creator-of-hono/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/the-story-of-web-framework-hono-from-the-creator-of-hono/&lt;/a&gt;) by Yusuke Wada&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sites Now Become Interactive 50% Faster&lt;/strong&gt; (&lt;a href="https://www.framer.com/blog/sites-interactive-faster/" rel="noopener noreferrer"&gt;https://www.framer.com/blog/sites-interactive-faster/&lt;/a&gt;) by Jacob Groß&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drag to Select&lt;/strong&gt; (&lt;a href="https://www.joshuawootonn.com/react-drag-to-select" rel="noopener noreferrer"&gt;https://www.joshuawootonn.com/react-drag-to-select&lt;/a&gt;) by Joshua Wootonn&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why I’m skeptical of rewriting JavaScript tools in “faster” languages&lt;/strong&gt; (&lt;a href="https://nolanlawson.com/2024/10/20/why-im-skeptical-of-rewriting-javascript-tools-in-faster-languages/" rel="noopener noreferrer"&gt;https://nolanlawson.com/2024/10/20/why-im-skeptical-of-rewriting-javascript-tools-in-faster-languages/&lt;/a&gt;) by Nolan Lawson&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 62
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Our Journey with Caching&lt;/strong&gt; (&lt;a href="https://nextjs.org/blog/our-journey-with-caching" rel="noopener noreferrer"&gt;https://nextjs.org/blog/our-journey-with-caching&lt;/a&gt;) by Sebastian Markbåge&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline UX Patterns&lt;/strong&gt; (&lt;a href="https://github.com/Expensify/App/blob/main/contributingGuides/OFFLINE_UX.md" rel="noopener noreferrer"&gt;https://github.com/Expensify/App/blob/main/contributingGuides/OFFLINE_UX.md&lt;/a&gt;) by Expensify&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;World Wide Web Foundation closes so Tim Berners-Lee can spend more time with his protocol&lt;/strong&gt; (&lt;a href="https://www.theregister.com/2024/09/30/world_wide_web_foundation_closes/" rel="noopener noreferrer"&gt;https://www.theregister.com/2024/09/30/world_wide_web_foundation_closes/&lt;/a&gt;) by Thomas Claburn&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;why I'm turning off Firefox ad tracking: the PPA paradox&lt;/strong&gt; (&lt;a href="https://blog.zgp.org/why-turn-off-firefox-ad-tracking/" rel="noopener noreferrer"&gt;https://blog.zgp.org/why-turn-off-firefox-ad-tracking/&lt;/a&gt;) by Don Marti&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hyper-Scale Activated! Ship Your Own FaaS 🤖&lt;/strong&gt; (&lt;a href="https://dev.to/smapiot/hyper-scale-activated-ship-your-own-faas-40kg"&gt;https://dev.to/smapiot/hyper-scale-activated-ship-your-own-faas-40kg&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/florianrappl"&gt;@florianrappl&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next.js 15&lt;/strong&gt; (&lt;a href="https://nextjs.org/blog/next-15" rel="noopener noreferrer"&gt;https://nextjs.org/blog/next-15&lt;/a&gt;) by Delba de Oliveira et al&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Data Center Latencies - Visualized&lt;/strong&gt; (&lt;a href="https://benjdd.com/aws/" rel="noopener noreferrer"&gt;https://benjdd.com/aws/&lt;/a&gt;) by Benjamin Dicken&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where web components shine&lt;/strong&gt; (&lt;a href="https://daverupert.com/2024/10/super-web-components-sunshine/" rel="noopener noreferrer"&gt;https://daverupert.com/2024/10/super-web-components-sunshine/&lt;/a&gt;) by Dave Rupert&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Netlify joins OpenNext&lt;/strong&gt; (&lt;a href="https://www.netlify.com/blog/netlify-joins-opennext/" rel="noopener noreferrer"&gt;https://www.netlify.com/blog/netlify-joins-opennext/&lt;/a&gt;) by Mathias Biilmann&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New Architecture is here&lt;/strong&gt; (&lt;a href="https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here" rel="noopener noreferrer"&gt;https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here&lt;/a&gt;) by The React Team&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 63
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Expo SDK 52 beta is now available&lt;/strong&gt; (&lt;a href="https://expo.dev/changelog/2024/10-24-sdk-52-beta" rel="noopener noreferrer"&gt;https://expo.dev/changelog/2024/10-24-sdk-52-beta&lt;/a&gt;) by Brent Vatne&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's Not an Abstraction, That's Just a Layer of Indirection&lt;/strong&gt; (&lt;a href="https://fhur.me/posts/2024/thats-not-an-abstraction" rel="noopener noreferrer"&gt;https://fhur.me/posts/2024/thats-not-an-abstraction&lt;/a&gt;) by Fernando Hurtado&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An Early Glimpse of TanStack Start&lt;/strong&gt; (&lt;a href="https://www.youtube.com/watch?v=AuHqwQsf64o" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=AuHqwQsf64o&lt;/a&gt;) by Tanner Lindsey&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How we shrunk our Javascript monorepo git size by 94%&lt;/strong&gt; (&lt;a href="https://www.jonathancreamer.com/how-we-shrunk-our-git-repo-size-by-94-percent/" rel="noopener noreferrer"&gt;https://www.jonathancreamer.com/how-we-shrunk-our-git-repo-size-by-94-percent/&lt;/a&gt;) by Jonathan Creamer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Making content-aware components using CSS :has(), grid, and quantity queries&lt;/strong&gt; (&lt;a href="https://piccalil.li/blog/making-content-aware-components-using-css-has-grid-and-quantity-queries/" rel="noopener noreferrer"&gt;https://piccalil.li/blog/making-content-aware-components-using-css-has-grid-and-quantity-queries/&lt;/a&gt;) by Eric Bailey&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remix Todo App: Part 1 - Building the App Layout and Structure&lt;/strong&gt; (&lt;a href="https://www.udohjeremiah.com/blog/remix-todo-app-part-1-building-the-app-layout-and-structure" rel="noopener noreferrer"&gt;https://www.udohjeremiah.com/blog/remix-todo-app-part-1-building-the-app-layout-and-structure&lt;/a&gt;) by Udoh Jeremiah&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Rust in Non-Rust Servers to Improve Performance&lt;/strong&gt; (&lt;a href="https://github.com/pretzelhammer/rust-blog/blob/master/posts/rust-in-non-rust-servers.md" rel="noopener noreferrer"&gt;https://github.com/pretzelhammer/rust-blog/blob/master/posts/rust-in-non-rust-servers.md&lt;/a&gt;) by Pretzelhammer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive Select component using Phoenix LiveView and vanilla JS Hook&lt;/strong&gt; (&lt;a href="https://aurmartin.fr/posts/phoenix-liveview-select/" rel="noopener noreferrer"&gt;https://aurmartin.fr/posts/phoenix-liveview-select/&lt;/a&gt;) by Aurélien Martin&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unleash the Power of Scroll-Driven Animations&lt;/strong&gt; (&lt;a href="https://css-tricks.com/unleash-the-power-of-scroll-driven-animations/" rel="noopener noreferrer"&gt;https://css-tricks.com/unleash-the-power-of-scroll-driven-animations/&lt;/a&gt;) by Geoff Graham&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incremental adoption of micro-frontends with Cloudflare Workers&lt;/strong&gt; (&lt;a href="https://blog.cloudflare.com/fragment-piercing/" rel="noopener noreferrer"&gt;https://blog.cloudflare.com/fragment-piercing/&lt;/a&gt;) by Peter Bacon Darwin, Dario Piotrowicz, James Culveyhouse, Igor Minar&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 64
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Headless, boneless, skinless &amp;amp; lifeless UI&lt;/strong&gt; (&lt;a href="https://nerdy.dev/headless-boneless-and-skinless-ui" rel="noopener noreferrer"&gt;https://nerdy.dev/headless-boneless-and-skinless-ui&lt;/a&gt;) by Adam Argyle&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On Crafting Painterly Shaders&lt;/strong&gt; (&lt;a href="https://blog.maximeheckel.com/posts/on-crafting-painterly-shaders/" rel="noopener noreferrer"&gt;https://blog.maximeheckel.com/posts/on-crafting-painterly-shaders/&lt;/a&gt;) by Maxime Heckel&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smarter than 'Ctrl+F': Linking Directly to Web Page Content&lt;/strong&gt; (&lt;a href="https://alfy.blog/2024/10/19/linking-directly-to-web-page-content.html" rel="noopener noreferrer"&gt;https://alfy.blog/2024/10/19/linking-directly-to-web-page-content.html&lt;/a&gt;) by Ahmad Alfy&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simplfying Islands Architecture&lt;/strong&gt; (&lt;a href="https://preactjs.com/blog/simplifying-islands-arch/" rel="noopener noreferrer"&gt;https://preactjs.com/blog/simplifying-islands-arch/&lt;/a&gt;) by Siddharth Gelera&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Going through the “State of CSS” results…&lt;/strong&gt; (&lt;a href="https://www.joshwcomeau.com/email/2024-10-22-css-survey/" rel="noopener noreferrer"&gt;https://www.joshwcomeau.com/email/2024-10-22-css-survey/&lt;/a&gt;) by Josh W. Comeau&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rendering Outlines with a Post-processing Shader&lt;/strong&gt; (&lt;a href="https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/" rel="noopener noreferrer"&gt;https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/&lt;/a&gt;) by Adam Wolf&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should JavaScript be split into two languages? New Google-driven proposal divides opinion&lt;/strong&gt; (&lt;a href="https://devclass.com/2024/10/22/should-javascript-be-split-into-two-languages-new-google-driven-proposal-divides-opinion/" rel="noopener noreferrer"&gt;https://devclass.com/2024/10/22/should-javascript-be-split-into-two-languages-new-google-driven-proposal-divides-opinion/&lt;/a&gt;) by Tim Anderson&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introducing Wasmer 5.0&lt;/strong&gt; (&lt;a href="https://wasmer.io/posts/introducing-wasmer-v5" rel="noopener noreferrer"&gt;https://wasmer.io/posts/introducing-wasmer-v5&lt;/a&gt;) by Syrus Akbary&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should masonry be part of CSS grid?&lt;/strong&gt; (&lt;a href="https://ishadeed.com/article/css-grid-masonry/" rel="noopener noreferrer"&gt;https://ishadeed.com/article/css-grid-masonry/&lt;/a&gt;) by Ahmad Shadeed&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recap: Next.js Conf 2024&lt;/strong&gt; (&lt;a href="https://vercel.com/blog/recap-next-js-conf-2024" rel="noopener noreferrer"&gt;https://vercel.com/blog/recap-next-js-conf-2024&lt;/a&gt;) by Lee Robinson, Delba de Oliveira&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 65
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How headless components became the future for building UI libraries&lt;/strong&gt; (&lt;a href="https://www.subframe.com/blog/how-headless-components-became-the-future-for-building-ui-libraries" rel="noopener noreferrer"&gt;https://www.subframe.com/blog/how-headless-components-became-the-future-for-building-ui-libraries&lt;/a&gt;) by Irvin Zhan&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hyperkit&lt;/strong&gt; (&lt;a href="https://www.chrsgrrtt.com/hyperkit" rel="noopener noreferrer"&gt;https://www.chrsgrrtt.com/hyperkit&lt;/a&gt;) by Chris Garrett&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Converting any website to a React component&lt;/strong&gt; (&lt;a href="https://www.magicpatterns.com/blog/any-website-to-react-component" rel="noopener noreferrer"&gt;https://www.magicpatterns.com/blog/any-website-to-react-component&lt;/a&gt;) by Teddy Ni&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML Form Validation is heavily underused&lt;/strong&gt; (&lt;a href="https://expressionstatement.com/html-form-validation-is-heavily-underused" rel="noopener noreferrer"&gt;https://expressionstatement.com/html-form-validation-is-heavily-underused&lt;/a&gt;) by everdimension&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using Shadcn UI without a Tailwind Config File&lt;/strong&gt; (&lt;a href="https://www.luisball.com/blog/shadcn-ui-with-tailwind-v4" rel="noopener noreferrer"&gt;https://www.luisball.com/blog/shadcn-ui-with-tailwind-v4&lt;/a&gt;) by Luis Ball&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Debugging Microservices &amp;amp; Distributed Systems&lt;/strong&gt; (&lt;a href="https://sentry.io/resources/debugging-microservices-and-distributed-systems/" rel="noopener noreferrer"&gt;https://sentry.io/resources/debugging-microservices-and-distributed-systems/&lt;/a&gt;) by Sarah Guthals&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RFC: An updated style guide for the year 2024&lt;/strong&gt; (&lt;a href="https://github.com/angular/angular/discussions/58412" rel="noopener noreferrer"&gt;https://github.com/angular/angular/discussions/58412&lt;/a&gt;) by Jeremy Elbourn&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My solar-powered and self-hosted website&lt;/strong&gt; (&lt;a href="https://dri.es/my-solar-powered-and-self-hosted-website" rel="noopener noreferrer"&gt;https://dri.es/my-solar-powered-and-self-hosted-website&lt;/a&gt;) by Dries Buytaert&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP Tunnel and Proxy in JavaScript&lt;/strong&gt; (&lt;a href="https://dbushell.com/2024/10/22/http-tunnel-proxy/" rel="noopener noreferrer"&gt;https://dbushell.com/2024/10/22/http-tunnel-proxy/&lt;/a&gt;) by David Bushell&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a Single-Page App?&lt;/strong&gt; (&lt;a href="https://jakelazaroff.com/words/whats-a-single-page-app/" rel="noopener noreferrer"&gt;https://jakelazaroff.com/words/whats-a-single-page-app/&lt;/a&gt;) by Jake Lazaroff&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These are all outstanding articles by masterful authors. I enjoyed reading them all - I hope you did find something in there, too.&lt;/p&gt;

&lt;p&gt;👉 Follow me on &lt;a href="https://www.linkedin.com/in/florian-rappl/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/FlorianRappl" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, or here for more to come.&lt;/p&gt;

&lt;p&gt;🙏 Thanks to all the authors and contributors for their hard work!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>frontend</category>
      <category>node</category>
    </item>
    <item>
      <title>Hyper-Scale Activated! Ship Your Own FaaS 🤖</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Fri, 18 Oct 2024 12:34:36 +0000</pubDate>
      <link>https://dev.to/smapiot/hyper-scale-activated-ship-your-own-faas-40kg</link>
      <guid>https://dev.to/smapiot/hyper-scale-activated-ship-your-own-faas-40kg</guid>
      <description>&lt;p&gt;In the past decade I've done a lot of work on distributed web applications. In that area you definitely require processes that work implicitly - fully decoupled, with autonomy for individual teams and modules.&lt;/p&gt;

&lt;p&gt;The key question quite often is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How awesome would it be to just extend your application at runtime without any downtime whatsoever?!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article I want to look at how we can extend a Node server using Express.js to act like a FaaS provider (such as AWS Lambda or Azure Functions).&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To follow this article you'll need the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js with npm installed (I recommend version 22 or higher - we'll use &lt;code&gt;fetch&lt;/code&gt; in Node, which is available since version 18)&lt;/li&gt;
&lt;li&gt;A code editor&lt;/li&gt;
&lt;li&gt;If you want to &lt;a href="https://github.com/piral-samples/piral-cloud-express-plugins-demo" rel="noopener noreferrer"&gt;clone the sample project&lt;/a&gt; you'll need to have &lt;code&gt;git&lt;/code&gt; installed (but you can download it as a ZIP from GitHub)&lt;/li&gt;
&lt;li&gt;Access to the &lt;a href="https://feed.piral.cloud" rel="noopener noreferrer"&gt;Piral Cloud Feed Service&lt;/a&gt; - you can use it for free using a Microsoft or GitHub account&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;Before we start to write some code let's look at the anticipated architecture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0owb6x5e1y9myj26v93m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0owb6x5e1y9myj26v93m.png" alt="Architecture diagram" width="491" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the &lt;a href="https://feed.piral.cloud" rel="noopener noreferrer"&gt;Piral Cloud Feed Service&lt;/a&gt; we get access to a dynamic service / module registry, which we can leverage to load a list of modules, evaluate them directly from their URLs, and then use them to populate a list of (sub-)routers.&lt;/p&gt;

&lt;p&gt;The routers are necessary to extend the default router middleware. Let's see the whole thing in action. We start by writing the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the Server
&lt;/h2&gt;

&lt;p&gt;As server we use a pretty standard and lightweight Express.js server. It can be created by running the following commands in a new directory.&lt;/p&gt;

&lt;p&gt;We first initialize a new npm project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we install the required dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We add a new file (&lt;em&gt;index.js&lt;/em&gt;) with the following content:&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;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&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;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello world!&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Running at http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we finally run the application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to localhost:3000 in the browser of your choice and see that everything is running as it should:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0e0q1t8rgqiqqm27lzdm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0e0q1t8rgqiqqm27lzdm.png" alt="Server running" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Great! But there is a bit more to it for making it truly dynamic...&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the Server Dynamic
&lt;/h2&gt;

&lt;p&gt;So what we are after is to run modules that we can just publish whenever we feel the need for it. The first thing to look for is to avoid having routing issues.&lt;/p&gt;

&lt;p&gt;To prevent different modules for occupying the same route we will actually namespace the routes by their module names. In Express.js this also allows us to use dynamic &lt;code&gt;Router&lt;/code&gt; objects, which are bound to their module names.&lt;/p&gt;

&lt;p&gt;In code this can be done like that:&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;const&lt;/span&gt; &lt;span class="nx"&gt;routers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/:namespace&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&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;next&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;params&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;namespace&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;router&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="nf"&gt;router&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;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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="nf"&gt;next&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;We introduce a middleware that is sensitive to a &lt;code&gt;namespace&lt;/code&gt; parameter. In a productive setup we might want to constraint this a bit further, e.g., &lt;code&gt;/api/:namespace&lt;/code&gt; or &lt;code&gt;/apps/:namespace&lt;/code&gt;, to avoid clashes between in-built functionality and the modules.&lt;/p&gt;

&lt;p&gt;The job of the middleware is to use the retrieved &lt;code&gt;namespace&lt;/code&gt; value by looking at all available sub-routers. If a sub-router is matched we will use the sub-router as follow-up middleware. Otherwise, we continue with the next middleware. Usually, the next middleware would be the "not-found-route" from Express.js.&lt;/p&gt;

&lt;p&gt;The question to answer now is how to populate the &lt;code&gt;routers&lt;/code&gt; object. How can we obtain these sub-routers?&lt;/p&gt;

&lt;p&gt;What we can do is to introduce a function to obtain the information from a discovery service. This service gets us all the relevant pieces of information - we then only need to load / run the referenced modules.&lt;/p&gt;

&lt;p&gt;The necessary code to built something like that is:&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;const&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://feed.piral.cloud/api/v1/pilet/express-demo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&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="s2"&gt;@&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadPlugins&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading plugins ...&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;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="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&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;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;for &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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;items&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Integrating plugin "&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="s2"&gt;" ...`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;installPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Integrated plugin "&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="s2"&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;watchPlugins&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;loadPlugins&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What are we doing here?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We want to start loading the plugins / sub-routers as fast as possible. So we immediately run the &lt;code&gt;loadPlugins&lt;/code&gt; function. Ideally we use the top-level &lt;code&gt;await&lt;/code&gt; here; if it's not available (or if we want to save a bit of startup time) we just call it as shown.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;loadPlugins&lt;/code&gt; fetches the information from the discovery service.&lt;/li&gt;
&lt;li&gt;Once the information is fetched we go over the contained &lt;code&gt;items&lt;/code&gt; array. Each item is treated like the meta information of a plugin - so we just call &lt;code&gt;installPlugin&lt;/code&gt; to use the found meta information.&lt;/li&gt;
&lt;li&gt;Finally we also want to react when something changes - so the &lt;code&gt;loadPlugins&lt;/code&gt; part concludes by watching for changes using the &lt;code&gt;watchPlugins&lt;/code&gt; function.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;/p&gt;
  Important Note
  &lt;br&gt;
If you want to follow this on your own then replace &lt;code&gt;https://feed.piral.cloud/api/v1/pilet/express-demo&lt;/code&gt; with your own discovery feed URL. Using the &lt;a href=""&gt;Piral Cloud Feed Service&lt;/a&gt; you can see the URL on the feed overview page.

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6s2r9k06ml4lkuibnndt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6s2r9k06ml4lkuibnndt.png" alt="Finding the Feed Service URL in the feed overview" width="800" height="397"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Let's look at the &lt;code&gt;installPlugin&lt;/code&gt; first:&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;const&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&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;installPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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="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="nx"&gt;link&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&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;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="p"&gt;}&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;loadModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;link&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;setup&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;routers&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;makeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doesn't look too complicated, does it? What's happening here?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We decompose the item - but we are only interested in the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;link&lt;/code&gt; (module reference / URL) of the module.&lt;/li&gt;
&lt;li&gt;We create a new Express.js &lt;code&gt;Router&lt;/code&gt; instance (the "sub-router").&lt;/li&gt;
&lt;li&gt;We load the respective module - it should get us a function called &lt;code&gt;setup&lt;/code&gt; (now this is a design choice; you could either name it differently, make it a &lt;code&gt;default&lt;/code&gt; export, or export a &lt;code&gt;Router&lt;/code&gt; instance directly...&lt;/li&gt;
&lt;li&gt;If we really obtained a &lt;code&gt;setup&lt;/code&gt; function we call it with the &lt;code&gt;Router&lt;/code&gt; instance. This can now be modified properly.&lt;/li&gt;
&lt;li&gt;Finally, the created router is assigned to the &lt;code&gt;routers&lt;/code&gt; object and the &lt;code&gt;current&lt;/code&gt; array is extended with the proper entry.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As you might have guessed the &lt;code&gt;current&lt;/code&gt; array is for book-keeping purposes. It allows us to easily patch / change the routers in case of changes. For this, we'll need to look at the &lt;code&gt;watchPlugins&lt;/code&gt; function:&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;const&lt;/span&gt; &lt;span class="nx"&gt;WebSocket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&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;changeEventTypes&lt;/span&gt; &lt;span class="o"&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;add-pilet&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;update-pilet&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;remove-pilet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watchPlugins&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Watching plugins ...&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;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&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;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;console&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="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&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;msg&lt;/span&gt; &lt;span class="o"&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;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&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;changeEventTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&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;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="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;removeItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;makeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;id&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;addItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;makeId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="k"&gt;for &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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;removeItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;uninstallPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;for &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;item&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;addItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;installPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this code to run we rely on the &lt;code&gt;ws&lt;/code&gt; library. So we'll need to install it first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i ws
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;ws&lt;/code&gt; active we can have a look at what the code does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We define some events we want to listen to. The provided events are send via the WebSocket channel in case the contained modules change.&lt;/li&gt;
&lt;li&gt;Once a message is received we transform it (from raw bytes to a JSON object) and check if the type matches one of the events defined in (1).&lt;/li&gt;
&lt;li&gt;We could now use the information from the event, but some events (e.g., &lt;code&gt;update-pilet&lt;/code&gt;) don't tell us what changed. In such cases we could be in a problematic zone. So we follow the safe route and just re-load all the modules.&lt;/li&gt;
&lt;li&gt;Finally, we need to compare the &lt;em&gt;current&lt;/em&gt; information from the discovery service vs the stored / previous information. For the changed (removed, updated) modules we remove them using the &lt;code&gt;uninstallPlugin&lt;/code&gt;. All the ones that are not yet instantiated we add them using the &lt;code&gt;installPlugin&lt;/code&gt; function.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;uninstallPlugin&lt;/code&gt; looks as follows:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;uninstallPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;routers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;item&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="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&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 was easy, right? But so far we avoided the most complicated topic. How are we evaluating those modules?&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluating Modules from URLs
&lt;/h2&gt;

&lt;p&gt;In the browser you can always evaluate ESMs - they are coming from an URL by default. The same is true in runtimes such as Deno. But in Node.js this is not possible. So should we give up?&lt;/p&gt;

&lt;p&gt;As it turns out evaluating URLs is actually not so difficult. We need 2 things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Code to use the &lt;code&gt;vm&lt;/code&gt; module. We need to rely on the &lt;code&gt;SourceTextModule&lt;/code&gt; to create modules on the fly.&lt;/li&gt;
&lt;li&gt;The experimental flag &lt;code&gt;--experimental-vm-modules&lt;/code&gt; for running Node. This way, the &lt;code&gt;SourceTextModule&lt;/code&gt; is available at runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using those two ways we can add the following code to our server:&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;const&lt;/span&gt; &lt;span class="nx"&gt;vm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vm&lt;/span&gt;&lt;span class="dl"&gt;"&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;linkModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;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="nx"&gt;url&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;content&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SourceTextModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&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;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;specifier&lt;/span&gt;&lt;span class="p"&gt;)&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;newUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;specifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;linkModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&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;mod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&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;mod&lt;/span&gt;&lt;span class="p"&gt;;&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;loadModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createContext&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="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;linkModule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&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="nx"&gt;namespace&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="nx"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to evaluate "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&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="nx"&gt;ex&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="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;Finally, we have everything together and can just run the server with the flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--experimental-vm-modules&lt;/span&gt; index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We should see output like this in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loading plugins ...
Running at http://localhost:3000
Integrating plugin "app1@2.0.0" ...
Integrated plugin "app1@2.0.0"!
Integrating plugin "app2@1.0.4" ...
(node:23052) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Integrated plugin "app2@1.0.4"!
Integrating plugin "app3@1.0.0" ...
Integrated plugin "app3@1.0.0"!
Watching plugins ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we are already including a couple of modules (referred to as plugins). Let's see what we can do here - how we can write, deploy, and update them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Functions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One Module
&lt;/h3&gt;

&lt;p&gt;The first example is a really simple one; just a single function to return a constant string:&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/foo&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from app1 v2: /foo&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Importantly, we initialize a new project and define the &lt;em&gt;package.json&lt;/em&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publish-microfrontend --url https://feed.piral.cloud/api/v1/pilet/express-demo --interactive"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publish-microfrontend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.6.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;publish-microfrontend&lt;/code&gt; script is used to deploy the module. This little script just packages the current project and invokes a POST against the provided discovery service URL. The &lt;code&gt;--interactive&lt;/code&gt; flag then allows us to dynamically log into the discovery service to deploy our function.&lt;/p&gt;

&lt;p&gt;We'll see how this process looks in a second. Let's look at another example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two Modules
&lt;/h3&gt;

&lt;p&gt;While publishing the package with a single module works as expected the question is: Does it also work with more modules? The answer is yes.&lt;/p&gt;

&lt;p&gt;Pretty much the same definition in the &lt;em&gt;package.json&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lib/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"publish-microfrontend --url https://feed.dev.piral.cloud/api/v1/pilet/express-demo --interactive"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publish-microfrontend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.6.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But now &lt;em&gt;lib/index.js&lt;/em&gt; is different:&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;compute&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;./other.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/compute&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;query&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;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;b&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="nf"&gt;isNaN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&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="nx"&gt;c&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;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;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="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Only numbers allowed.`&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;Not only is this a bit more logic - it also uses a function from another module located at &lt;em&gt;lib/other.js&lt;/em&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;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;a&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;b&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="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="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&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="kc"&gt;NaN&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;As you might have guessed our ESM URL loader just works as it should. No questions asked. Great!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhs1gkr4elzp5ovt855h.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhs1gkr4elzp5ovt855h.gif" alt="Impressive" width="500" height="281"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We won't answer the burning question: Can it do three modules? I guess we know the answer, but if you want to find out get active now!&lt;/p&gt;

&lt;p&gt;In the meantime we go already to the complex case of having the modules written in TypeScript - and including some dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript Modules with Dependencies
&lt;/h3&gt;

&lt;p&gt;Consider the code at &lt;em&gt;src/index.ts&lt;/em&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;cors&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;cors&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Router&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;express-serve-static-core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

  &lt;span class="nx"&gt;router&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`NOT FOUND.`&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;Here we are using types and dependencies. This won't work - we don't do any dependency resolution and we don't transpile TypeScript. So should we be worried? Not at all!&lt;/p&gt;

&lt;p&gt;Let's bundle it using esbuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;esbuild &lt;span class="nt"&gt;--outdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dist src/index.ts &lt;span class="nt"&gt;--bundle&lt;/span&gt; &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;node &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;esm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even better - we can just extend the &lt;em&gt;package.json&lt;/em&gt; to take care of this in the &lt;code&gt;deploy&lt;/code&gt; task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"esbuild --outdir=dist src/index.ts --bundle --platform=node --format=esm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build &amp;amp;&amp;amp; publish-microfrontend --url https://feed.dev.piral.cloud/api/v1/pilet/express-demo --interactive"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/cors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.8.17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@types/express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^5.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"esbuild"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.24.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"publish-microfrontend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.6.2"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.8.5"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, we can just write anything - bundle it using &lt;code&gt;esbuild&lt;/code&gt; and ship it with &lt;code&gt;publish-microfrontend&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now let's step back a bit and see how we actually deploy the modules:&lt;/p&gt;

&lt;h3&gt;
  
  
  Deployment
&lt;/h3&gt;

&lt;p&gt;Maybe let's see first how it looks in the discovery service when we deployed all of our modules:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1c541s2pz9etr7v8qwt8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1c541s2pz9etr7v8qwt8.png" alt="Overview of modules" width="800" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As already mentioned for deploying such a module we can use the available &lt;code&gt;publish-microfrontend&lt;/code&gt; npm package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx publish-microfrontend &lt;span class="nt"&gt;--url&lt;/span&gt; &amp;lt;your-discovery-service-url&amp;gt; &lt;span class="nt"&gt;--api-key&lt;/span&gt; &amp;lt;your-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In cases such as the ones above we might swap the &lt;code&gt;--api-key&lt;/code&gt; with an &lt;code&gt;--interactive&lt;/code&gt; flow, where we need to use the browser for obtaining a user token instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx publish-microfrontend &lt;span class="nt"&gt;--url&lt;/span&gt; &amp;lt;your-discovery-service-url&amp;gt; &lt;span class="nt"&gt;--interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's see how this process looks in real life:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8p39e4ejphb5ysrznaco.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8p39e4ejphb5ysrznaco.gif" alt="The publish process in motion" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the end having such a powerful service registry between our server and the respective modules is not only clean, but also useful for flexibility reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Dynamics
&lt;/h2&gt;

&lt;p&gt;We already saw that the dynamics with respect to partial rollbacks are in place from the beginning. In general, the service registry's portal gives us the ability to control every bit of the delivery process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxobl0sbqeqblhbjdww5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxxobl0sbqeqblhbjdww5.png" alt="Version selector" width="800" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The version selector in particular is quite useful. Besides the possibility of making rollbacks, we can also do feature flags, A/B testing, blue-green deployments, or canary releases.&lt;/p&gt;

&lt;p&gt;Quite powerful stuff...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5y45mlet4a9ut9phpmgq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5y45mlet4a9ut9phpmgq.gif" alt="Homelander" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This allows us, e.g., to have multiple instances of our Express.js server, where each instance might get a different set of modules. Sounds crazy (how can this be useful?!) but if you don't consider these instances equal (i.e., meant for scaling), but rather as different sites (e.g., one for US, one for EU etc.) then backend behavior might be different - at least in some areas.&lt;/p&gt;

&lt;p&gt;Beforehand, that was a lot of overhead. You had to produce countless variations of the same server - now you can just have one and have the different behavior qualified through the loaded modules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article you've seen how you can easily extend your Express.js server into a FaaS platform allowing you to add, update, and remove endpoints on the fly.&lt;/p&gt;

&lt;p&gt;Of course, the provided sample is rather simplistic and does not solve some of the issues including advanced isolation, memory footprint (unload and restrictions of modules), local module development using an emulator, and many others. But it's a very convenient start - showing what's possible with just a few lines of code.&lt;/p&gt;

&lt;p&gt;Check out the sample code at &lt;a href="https://github.com/piral-samples/piral-cloud-express-plugins-demo" rel="noopener noreferrer"&gt;github.com/piral-samples/piral-cloud-express-plugins-demo&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>express</category>
    </item>
    <item>
      <title>🎃 50 Articles to Avoid Web Dev Nightmares</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Tue, 15 Oct 2024 13:28:34 +0000</pubDate>
      <link>https://dev.to/florianrappl/50-articles-to-web-dev-nightmares-15ah</link>
      <guid>https://dev.to/florianrappl/50-articles-to-web-dev-nightmares-15ah</guid>
      <description>&lt;p&gt;This year I started a new series on &lt;a href="https://www.linkedin.com/in/florian-rappl/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; - "Advanced Links for Frontend". Each issue has 10 links to outstanding posts / articles. This bundle contains the links from the last 5 issues (issue 56 to issue 60).&lt;/p&gt;

&lt;p&gt;I hope you enjoy this collection. Let me know in the comments which of these articles is your favorite (and why).&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 56
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The CSS radial composer&lt;/strong&gt; (&lt;a href="https://zumerlab.github.io/orbit-docs/" rel="noopener noreferrer"&gt;https://zumerlab.github.io/orbit-docs/&lt;/a&gt;) by Orbit&lt;br&gt;
Gravity is everywhere! For some reason I feel attracted to this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remix's concurrent submissions are fundamentally flawed&lt;/strong&gt; (without causal ordering) (&lt;a href="https://dashbit.co/blog/remix-concurrent-submissions-flawed" rel="noopener noreferrer"&gt;https://dashbit.co/blog/remix-concurrent-submissions-flawed&lt;/a&gt;) by José Valim&lt;br&gt;
Honestly, I am not surprised.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ts-blank-space&lt;/strong&gt; (&lt;a href="https://bloomberg.github.io/ts-blank-space/" rel="noopener noreferrer"&gt;https://bloomberg.github.io/ts-blank-space/&lt;/a&gt;) by Ashley Claymore&lt;br&gt;
This can be really useful, but I wonder why not just esbuild is used. It's fast and can also be used for more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;date-fns v4.0 is out with first-class time zones support!&lt;/strong&gt; (&lt;a href="https://blog.date-fns.org/v40-with-time-zone-support/" rel="noopener noreferrer"&gt;https://blog.date-fns.org/v40-with-time-zone-support/&lt;/a&gt;) by Sasha Koss&lt;br&gt;
Well, having time zones support is one of the things I'd consider necessary for any good time library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fastify v5 is Now Officially Released!&lt;/strong&gt; (&lt;a href="https://openjsf.org/blog/fastifys-growth-and-success" rel="noopener noreferrer"&gt;https://openjsf.org/blog/fastifys-growth-and-success&lt;/a&gt;) by Fastify Team&lt;br&gt;
Fastify is great and presumably under-appreciated in the Node.js community.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebKit Features in Safari 18.0&lt;/strong&gt; (&lt;a href="https://webkit.org/blog/15865/webkit-features-in-safari-18-0/" rel="noopener noreferrer"&gt;https://webkit.org/blog/15865/webkit-features-in-safari-18-0/&lt;/a&gt;) by Jen Simmons&lt;br&gt;
Keep an eye on this - view transitions and WebXR are huge! Fantastic release!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Biome v1.9 Anniversary Release&lt;/strong&gt; (&lt;a href="https://biomejs.dev/blog/biome-v1-9/" rel="noopener noreferrer"&gt;https://biomejs.dev/blog/biome-v1-9/&lt;/a&gt;) by Victorien Elvinger&lt;br&gt;
Alright I can happily say that finally Biome is also becoming interesting to me. Goodbye Prettier + eslint!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A milestone for TypeScript Performance in TanStack Router&lt;/strong&gt; (&lt;a href="https://tanstack.com/blog/tanstack-router-typescript-performance" rel="noopener noreferrer"&gt;https://tanstack.com/blog/tanstack-router-typescript-performance&lt;/a&gt;) by Christopher Horobin&lt;br&gt;
Great write-up with some insights on how the type checker / TypeScript performance can be evaluated and improved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New Values and Functions in CSS&lt;/strong&gt; (&lt;a href="https://dev.to/alvaromontoro/new-values-and-functions-in-css-1b9o"&gt;https://dev.to/alvaromontoro/new-values-and-functions-in-css-1b9o&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/alvaromontoro"&gt;@alvaromontoro&lt;/a&gt; &lt;br&gt;
Well, some of these functions are not really new, but it's always good to be reminded about them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deno 2.0 Release Candidate&lt;/strong&gt; (&lt;a href="https://deno.com/blog/v2.0-release-candidate" rel="noopener noreferrer"&gt;https://deno.com/blog/v2.0-release-candidate&lt;/a&gt;) by Andy Jiang and Bartek Iwańczuk&lt;br&gt;
The v2 promises to be a really well-rounded release. Time for everyone to take another look at Deno!&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 57
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;How Discord Reduced Websocket Traffic by 40%&lt;/strong&gt; (&lt;a href="https://discord.com/blog/how-discord-reduced-websocket-traffic-by-40-percent" rel="noopener noreferrer"&gt;https://discord.com/blog/how-discord-reduced-websocket-traffic-by-40-percent&lt;/a&gt;) by Austin Whyte&lt;br&gt;
A lot of micro optimizations and lazy loading (or "sending") for the win!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You Can't Build Interactive Web Apps Except as Single Page Applications... And Other Myths&lt;/strong&gt; (&lt;a href="https://htmx.org/essays/you-cant/" rel="noopener noreferrer"&gt;https://htmx.org/essays/you-cant/&lt;/a&gt;) by Tony Alaribe&lt;br&gt;
Never heard that myth - but it's fine for me to claim that fire has been rediscovered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Component Composition is great btw&lt;/strong&gt; (&lt;a href="https://tkdodo.eu/blog/component-composition-is-great-btw" rel="noopener noreferrer"&gt;https://tkdodo.eu/blog/component-composition-is-great-btw&lt;/a&gt;) by Dominik Dorfmeister&lt;br&gt;
Truly. What's also great is to have the right company boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compile and run C in JavaScript&lt;/strong&gt; (&lt;a href="https://bun.sh/blog/compile-and-run-c-in-js" rel="noopener noreferrer"&gt;https://bun.sh/blog/compile-and-run-c-in-js&lt;/a&gt;) by Jarred Sumner&lt;br&gt;
More like: Compile and run C in Bun. As usual, I see the devil being in the detail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Nine Node Pillars&lt;/strong&gt; (&lt;a href="https://www.platformatichq.com/node-principles" rel="noopener noreferrer"&gt;https://www.platformatichq.com/node-principles&lt;/a&gt;) by Matteo Collina et al.&lt;br&gt;
It's great to recap those pillars, even though I did not find anything really Node.js specific (most of the points could be assigned on to any web server).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Module Federation Vite the new v1 is here 🎉&lt;/strong&gt; (&lt;a href="https://www.learnbydo.ing/diary/2024-09-23-module-federation-vite-v1" rel="noopener noreferrer"&gt;https://www.learnbydo.ing/diary/2024-09-23-module-federation-vite-v1&lt;/a&gt;) by Giorgio Boa&lt;br&gt;
Happy to see that progress is made! Now we only miss all the other bundlers ...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Golden Ratio in CSS&lt;/strong&gt; (&lt;a href="https://dev.to/madsstoumann/the-golden-ratio-in-css-53d0"&gt;https://dev.to/madsstoumann/the-golden-ratio-in-css-53d0&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/madsstoumann"&gt;@madsstoumann&lt;/a&gt; &lt;br&gt;
Sweet - starts with some boxes and ends with a lot of rings. I love it!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sticky Transitioning Veggie Hamburger 🍔&lt;/strong&gt; (&lt;a href="https://dev.to/florianrappl/sticky-transitioning-veggie-hamburger-7dd"&gt;https://dev.to/florianrappl/sticky-transitioning-veggie-hamburger-7dd&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/florianrappl"&gt;@florianrappl&lt;/a&gt; &lt;br&gt;
Put your web app on a diet and reduce the amount of JS - especially when not needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Typed Linting Needs TypeScript Today&lt;/strong&gt; (&lt;a href="https://www.joshuakgoldberg.com/blog/why-typed-linting-needs-typescript-today/" rel="noopener noreferrer"&gt;https://www.joshuakgoldberg.com/blog/why-typed-linting-needs-typescript-today/&lt;/a&gt;) by Josh Goldberg&lt;br&gt;
Every once in a while somebody tries to reimplement TypeScript - so far everyone failed. Yes, TypeScript is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Improving rendering performance with CSS content-visibility&lt;/strong&gt; (&lt;a href="https://nolanlawson.com/2024/09/18/improving-rendering-performance-with-css-content-visibility/" rel="noopener noreferrer"&gt;https://nolanlawson.com/2024/09/18/improving-rendering-performance-with-css-content-visibility/&lt;/a&gt;) by Nolan Lawson&lt;br&gt;
This is indeed magical. I see some difficulties here, but certainly no less than doing actual lazy loading / virtualization in JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 58
&lt;/h2&gt;

&lt;p&gt;Ever faced the problem of dead space through gaps using flexbox or grid? Then read about&lt;br&gt;
&lt;strong&gt;Sanding UI&lt;/strong&gt; (&lt;a href="https://blog.jim-nielsen.com/2024/sanding-ui/" rel="noopener noreferrer"&gt;https://blog.jim-nielsen.com/2024/sanding-ui/&lt;/a&gt;) by Jim Nielsen&lt;/p&gt;

&lt;p&gt;As you are all aware Ahmad has become my favorite author for CSS related articles. Recently he gave his website some polish. Read about the&lt;br&gt;
&lt;strong&gt;Redesign Case Study&lt;/strong&gt; (&lt;a href="https://ishadeed.com/article/redesign-2024/" rel="noopener noreferrer"&gt;https://ishadeed.com/article/redesign-2024/&lt;/a&gt;) by Ahmad Shadeed&lt;/p&gt;

&lt;p&gt;Now that we conquered client islands it's time for server islands to become standard. One way is discussed in&lt;br&gt;
&lt;strong&gt;Partial Prerendering for Everyone with Cloudflare Workers&lt;/strong&gt; (&lt;a href="https://sunilpai.dev/posts/ppr-for-everyone/" rel="noopener noreferrer"&gt;https://sunilpai.dev/posts/ppr-for-everyone/&lt;/a&gt;) by Sunil Pai&lt;/p&gt;

&lt;p&gt;It was quite a week for web components. We started with&lt;br&gt;
&lt;strong&gt;Web Components Are Not the Future&lt;/strong&gt; (&lt;a href="https://dev.to/ryansolid/web-components-are-not-the-future-48bh"&gt;https://dev.to/ryansolid/web-components-are-not-the-future-48bh&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/ryansolid"&gt;@ryansolid&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;But as usual on this topic there had to be different opinions. Entering&lt;br&gt;
&lt;strong&gt;Web Components Are Not the Future — They’re the Present&lt;/strong&gt; (&lt;a href="https://www.abeautifulsite.net/posts/web-components-are-not-the-future-they-re-the-present/" rel="noopener noreferrer"&gt;https://www.abeautifulsite.net/posts/web-components-are-not-the-future-they-re-the-present/&lt;/a&gt;) by Cory LaViska&lt;/p&gt;

&lt;p&gt;In the end both have their points and perspectives, but like the old XKCD comic it was the job of a third person to summarize in&lt;br&gt;
&lt;strong&gt;Web components are okay&lt;/strong&gt; (&lt;a href="https://nolanlawson.com/2024/09/28/web-components-are-okay/" rel="noopener noreferrer"&gt;https://nolanlawson.com/2024/09/28/web-components-are-okay/&lt;/a&gt;) by Nolan Lawson&lt;/p&gt;

&lt;p&gt;Years ago we tried to introduce scoping in CSS - but without much success. Now the working group is back with power... Let's try&lt;br&gt;
&lt;strong&gt;Understanding the Concept of Scoping in CSS&lt;/strong&gt; (&lt;a href="https://dev.to/bilkeesu96/understanding-the-concept-of-scoping-in-css-1m9g"&gt;https://dev.to/bilkeesu96/understanding-the-concept-of-scoping-in-css-1m9g&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/bilkeesu96"&gt;@bilkeesu96&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;A major downside of classic cron jobs is the failure cascade. This can be countered by some worker frameworks - for instance offering something dubbed durable functions.&lt;br&gt;
&lt;strong&gt;What are Durable Functions? A visual JavaScript primer&lt;/strong&gt; (&lt;a href="https://www.inngest.com/blog/durable-functions-a-visual-javascript-primer" rel="noopener noreferrer"&gt;https://www.inngest.com/blog/durable-functions-a-visual-javascript-primer&lt;/a&gt;) by Lydia Hallie&lt;/p&gt;

&lt;p&gt;Having tests is both - a pain and necessity. Especially in the frontend things tend to become difficult - relying on the browser and user behavior for many features. Let's recap&lt;br&gt;
&lt;strong&gt;Testing Frontend — Lessons from over a million lines of TypeScript at Palantir&lt;/strong&gt; (&lt;a href="https://www.meticulous.ai/blog/lessons-from-a-decade" rel="noopener noreferrer"&gt;https://www.meticulous.ai/blog/lessons-from-a-decade&lt;/a&gt;) by Quentin Spencer-Harper&lt;/p&gt;

&lt;p&gt;Not only the web component department delivered... If you missed the excitement delivered by the WordPress community then read&lt;br&gt;
&lt;strong&gt;The messy WordPress drama, explained&lt;/strong&gt; (&lt;a href="https://www.theverge.com/2024/9/27/24256361/wordpress-wp-engine-drama-explained-matt-mullenweg" rel="noopener noreferrer"&gt;https://www.theverge.com/2024/9/27/24256361/wordpress-wp-engine-drama-explained-matt-mullenweg&lt;/a&gt;) by Emma Roth&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 59
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Building a Single-Page App with htmx&lt;/strong&gt; (&lt;a href="https://jakelazaroff.com/words/building-a-single-page-app-with-htmx/" rel="noopener noreferrer"&gt;https://jakelazaroff.com/words/building-a-single-page-app-with-htmx/&lt;/a&gt;) by Jake Lazaroff&lt;br&gt;
Your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keeping an eye on changes to MDN’s Browser Compatibility Data&lt;/strong&gt; (&lt;a href="https://bcd-watch.igalia.com/" rel="noopener noreferrer"&gt;https://bcd-watch.igalia.com/&lt;/a&gt;) by igalia&lt;br&gt;
This is quite neat - not only knowing what changed, but also when it changed.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;OpenFreeMap *&lt;/em&gt;(&lt;a href="https://openfreemap.org/" rel="noopener noreferrer"&gt;https://openfreemap.org/&lt;/a&gt;) by Zsolt Ero&lt;br&gt;
Eagerly waiting to use this with Leaflet - but the Mapbox demos look great, too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bridging the hard and the soft&lt;/strong&gt; (&lt;a href="https://wattenberger.com/thoughts/hard-and-soft" rel="noopener noreferrer"&gt;https://wattenberger.com/thoughts/hard-and-soft&lt;/a&gt;) by Amelia Wattenberger&lt;br&gt;
Superb read - it's all about the interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ESLint now officially supports linting of JSON and Markdown&lt;/strong&gt; (&lt;a href="https://eslint.org/blog/2024/10/eslint-json-markdown-support/" rel="noopener noreferrer"&gt;https://eslint.org/blog/2024/10/eslint-json-markdown-support/&lt;/a&gt;) by Nicholas Zakas&lt;br&gt;
I am not getting enough warnings already.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tauri 2.0 Stable Release&lt;/strong&gt; (&lt;a href="https://v2.tauri.app/blog/tauri-20/" rel="noopener noreferrer"&gt;https://v2.tauri.app/blog/tauri-20/&lt;/a&gt;) by Tillmann Weidinger&lt;br&gt;
It's now just a much better DX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running Clang in the browser using WebAssembly&lt;/strong&gt; (&lt;a href="https://wasmer.io/posts/clang-in-browser" rel="noopener noreferrer"&gt;https://wasmer.io/posts/clang-in-browser&lt;/a&gt;) by Syrus Akbary&lt;br&gt;
I need to repeat: Your scientists were so preoccupied with whether or not they could, they didn't stop to think if they should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unleash JavaScript's Potential with Functional Programming&lt;/strong&gt; (&lt;a href="https://janhesters.com/blog/unleash-javascripts-potential-with-functional-programming" rel="noopener noreferrer"&gt;https://janhesters.com/blog/unleash-javascripts-potential-with-functional-programming&lt;/a&gt;) by Jan Hesters&lt;br&gt;
Personal opinion: Don't get too much into FP, but just take the best pieces and use them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Local-First Case Study&lt;/strong&gt; (&lt;a href="https://jakelazaroff.com/words/a-local-first-case-study/" rel="noopener noreferrer"&gt;https://jakelazaroff.com/words/a-local-first-case-study/&lt;/a&gt;) by Jake Lazaroff&lt;br&gt;
Local-first is underappreciated but should be a cornerstone for good DX.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Gumroad Didn't Choose htmx&lt;/strong&gt; (&lt;a href="https://htmx.org/essays/why-gumroad-didnt-choose-htmx/" rel="noopener noreferrer"&gt;https://htmx.org/essays/why-gumroad-didnt-choose-htmx/&lt;/a&gt;) by Sahil Lavingia&lt;br&gt;
It's fine. No one needs to choose everything. Everything has trade-offs and I believe in choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Issue 60
&lt;/h2&gt;

&lt;p&gt;If you look for icons to be used in your next project - then maybe start directly at&lt;br&gt;
&lt;strong&gt;15+ Best Icon Libraries of 2024&lt;/strong&gt; (&lt;a href="https://dev.to/vinishbhaskar/best-icon-libraries-28ce"&gt;https://dev.to/vinishbhaskar/best-icon-libraries-28ce&lt;/a&gt;) by &lt;a class="mentioned-user" href="https://dev.to/vinishbhaskar"&gt;@vinishbhaskar&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Ever wondered how to get or set up a great CRM for free? if so, check out&lt;br&gt;
&lt;strong&gt;Atomic CRM&lt;/strong&gt; (&lt;a href="https://marmelab.com/atomic-crm/" rel="noopener noreferrer"&gt;https://marmelab.com/atomic-crm/&lt;/a&gt;) by Marmelab&lt;/p&gt;

&lt;p&gt;In case you missed the preview you can now enjoy the full next gen Node.js competitor;&lt;br&gt;
&lt;strong&gt;Announcing Deno 2&lt;/strong&gt; (&lt;a href="https://deno.com/blog/v2.0" rel="noopener noreferrer"&gt;https://deno.com/blog/v2.0&lt;/a&gt;) by Ryan Dahl&lt;/p&gt;

&lt;p&gt;Are you sometimes hesitant to just install npm packages? Paranoid, maybe not - here are some&lt;br&gt;
&lt;strong&gt;Nightmares on npm: How Two Malicious Packages Facilitate Data Theft and Destruction&lt;/strong&gt; (&lt;a href="https://socket.dev/blog/nightmares-on-npm-how-two-malicious-packages-facilitate-data-theft-and-destruction" rel="noopener noreferrer"&gt;https://socket.dev/blog/nightmares-on-npm-how-two-malicious-packages-facilitate-data-theft-and-destruction&lt;/a&gt;) by Kush Pandya&lt;/p&gt;

&lt;p&gt;I think there is a great new framework in town (no week without it!) - server-first with JSX to web components for islands of interactivity. Entering&lt;br&gt;
&lt;strong&gt;The Web Platform Framework&lt;/strong&gt; (&lt;a href="https://brisa.build" rel="noopener noreferrer"&gt;https://brisa.build&lt;/a&gt;) by Aral Roca Gomez&lt;/p&gt;

&lt;p&gt;Ever thought that logs can provide much more insights, but are too clumsy to work with? Maybe strict sequential logs are not the answer. One approach can be found by reading an&lt;br&gt;
&lt;strong&gt;Introduction to Causal Logs&lt;/strong&gt; (&lt;a href="https://joelgustafson.com/posts/2024-09-30/introduction-to-causal-logs" rel="noopener noreferrer"&gt;https://joelgustafson.com/posts/2024-09-30/introduction-to-causal-logs&lt;/a&gt;) by Joel Gustafson&lt;/p&gt;

&lt;p&gt;Besides having a new framework each week there is also another attempt on explaining and visualizing how React renders (or reconciliates) your view - so let's dive into&lt;br&gt;
&lt;strong&gt;The Interactive Guide to Rendering in React&lt;/strong&gt; (&lt;a href="https://ui.dev/why-react-renders" rel="noopener noreferrer"&gt;https://ui.dev/why-react-renders&lt;/a&gt;) by UiDev&lt;/p&gt;

&lt;p&gt;Do you believe that JavaScript on the server is essentially PHP? I did not - but if you did then read&lt;br&gt;
&lt;strong&gt;React on the server is not PHP&lt;/strong&gt; (&lt;a href="https://www.artmann.co/articles/react-on-the-server-is-not-php" rel="noopener noreferrer"&gt;https://www.artmann.co/articles/react-on-the-server-is-not-php&lt;/a&gt;) by Christoffer Artmann&lt;/p&gt;

&lt;p&gt;Do you like mysteries? Then try solving the riddle of&lt;br&gt;
&lt;strong&gt;Node.js, pipes, and disappearing bytes&lt;/strong&gt; (&lt;a href="https://sxlijin.github.io/2024-10-09-node-stdout-disappearing-bytes" rel="noopener noreferrer"&gt;https://sxlijin.github.io/2024-10-09-node-stdout-disappearing-bytes&lt;/a&gt;) by Sam Lijin&lt;/p&gt;

&lt;p&gt;I have a feeling that the server-side story for React gets more fragmented and unnecessarily complex each day - even without talking about&lt;br&gt;
&lt;strong&gt;Serverless servers and the challenge of new React architecture&lt;/strong&gt; (&lt;a href="https://bobaekang.com/blog/serverless-servers-and-the-challenge-of-new-react-architecture/" rel="noopener noreferrer"&gt;https://bobaekang.com/blog/serverless-servers-and-the-challenge-of-new-react-architecture/&lt;/a&gt;) by Bobae Kang&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;These are all outstanding articles by masterful authors. I enjoyed reading them all - I hope you did find something in there, too.&lt;/p&gt;

&lt;p&gt;👉 Follow me on &lt;a href="https://www.linkedin.com/in/florian-rappl/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;, &lt;a href="https://twitter.com/FlorianRappl" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;, or here for more to come.&lt;/p&gt;

&lt;p&gt;🙏 Thanks to all the authors and contributors for their hard work!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>node</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Sticky Transitioning Veggie Hamburger 🍔</title>
      <dc:creator>Florian Rappl</dc:creator>
      <pubDate>Wed, 25 Sep 2024 08:40:42 +0000</pubDate>
      <link>https://dev.to/florianrappl/sticky-transitioning-veggie-hamburger-7dd</link>
      <guid>https://dev.to/florianrappl/sticky-transitioning-veggie-hamburger-7dd</guid>
      <description>&lt;p&gt;In our efforts to make the page &lt;em&gt;faster&lt;/em&gt; and &lt;strong&gt;even faster&lt;/strong&gt; we also explored different ways of just omitting user interactivity altogether. One place we looked at intensively was the header (or general layout). The header contained a bit of interactivity in form of a React component.&lt;/p&gt;

&lt;p&gt;The job of the header component is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on mobile show a persistent header bar with a hamburger instead of the normal entries&lt;/li&gt;
&lt;li&gt;on desktop show all the menu entries, but switch the appearance once scrolling occurs (full height to less height with a shadow below)&lt;/li&gt;
&lt;li&gt;the menu on mobile should not have any scrolling (i.e., remove scrollbar if any)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice this looks like the following on desktop:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc2qhgrso198o5epq8vxk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc2qhgrso198o5epq8vxk.gif" alt="Desktop header"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And for mobile the appearance changes to this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5q2z9593csqbuin46tsk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5q2z9593csqbuin46tsk.gif" alt="Mobile header"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's see how we went from the original to a non-JS version that behaves the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Original Meat Burger
&lt;/h2&gt;

&lt;p&gt;Alright, we start with the original version of our header (the "hamburger"). To stay rather generic for this article we prepared a small sample that evolves with every step.&lt;/p&gt;

&lt;p&gt;We start with the original:&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/vitejs-vite-tpfcpd?embed=1&amp;amp;file=src%2FPageHeader.jsx&amp;amp;view=editor" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Most of the code is in the CSS-in-JS. You don't need to use the CSS-in-JS here (we actually use SASS), but it easily brings the two parts (CSS and React) that are needed for the component together.&lt;/p&gt;

&lt;p&gt;The critical part is that within the component we need to use some hooks (i.e., interactivity) to make it work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PageHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStickyHeader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;useLockBodyScroll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is thus to reduce it. Let's start with the easy thing first: How to get the hamburger menu working without JS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less Meat - Same Taste
&lt;/h2&gt;

&lt;p&gt;How can we toggle two states using CSS and no JS? Well, first we need to understand that CSS inherently allows us to distinguish between states. For instance, CSS allows to select based on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;empty&lt;/li&gt;
&lt;li&gt;hovered&lt;/li&gt;
&lt;li&gt;focused&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and more states directly. Among those are also special states of some form fields, e.g., if a checkbox is checked.&lt;/p&gt;

&lt;p&gt;So instead of toggling based on a &lt;code&gt;data-&lt;/code&gt; attribute, i.e., the following code in CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;"true"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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;we can toggle this based on another selector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;header&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;#header-hamburger-open&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&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;This assumes that a new element with the ID &lt;code&gt;header-hamburger-open&lt;/code&gt; has been introduced. This is a checkbox that is hidden to the user. The effect of hiding the checkbox can be done quite easily using CSS, too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nf"&gt;#header-hamburger-open&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;collapse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;Personally, I like to use &lt;code&gt;visibility: collapse&lt;/code&gt; as it transports the meaning the best. Unfortunately, for most elements Webkit-based/derived browsers will behave for &lt;code&gt;collapse&lt;/code&gt; like for &lt;code&gt;hidden&lt;/code&gt; - i.e., reserving the original space but not rendering the element.&lt;/p&gt;

&lt;p&gt;Only Firefox behaves in a way that aligns with the original meaning (but contrary to the spec - which is actually weird in this scenario). That's why we need the additional &lt;code&gt;height: 0&lt;/code&gt; to also prevent allocating same space on Webkit-based browsers such as Chrome.&lt;/p&gt;

&lt;p&gt;With this in mind we replace the original &lt;code&gt;button&lt;/code&gt; inside the &lt;code&gt;navbar-toggler&lt;/code&gt; with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"checkbox"&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"header-hamburger-open"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"navbar-toggler"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;
    &lt;span class="na"&gt;htmlFor&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"header-hamburger-open"&lt;/span&gt;
    &lt;span class="na"&gt;data-toggle&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"collapse"&lt;/span&gt;
    &lt;span class="na"&gt;aria-controls&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"navbar-menu-content"&lt;/span&gt;
    &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Toggle navigation"&lt;/span&gt;
  &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    🍔
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When clicking on the &lt;code&gt;label&lt;/code&gt; the &lt;code&gt;htmlFor&lt;/code&gt; attribute is used by the browser. This will lead to toggling the checkbox, which - as per our CSS structure - will toggle the menu.&lt;/p&gt;

&lt;p&gt;You can play around with this in the updated sample.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/vitejs-vite-zdvtvx?embed=1&amp;amp;file=src%2FPageHeader.jsx&amp;amp;view=editor" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Now that the easy part is done - let's look at the final step for making the truly JS-free: Having the sticky positioning fulfilled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Veggie Version - Original Taste
&lt;/h2&gt;

&lt;p&gt;The main complication when moving the sticky header from a JS transition to a CSS one is that CSS has no idea about scrolling.&lt;/p&gt;

&lt;p&gt;Let's see the original hook / code to trigger the transition:&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;useStickyHeader&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;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useRef&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="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useLayoutEffect&lt;/span&gt;&lt;span class="p"&gt;(()&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;sticky&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetTop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onscroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageYOffset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sticky&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sticky&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="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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onscroll&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="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;header&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;So by applying the CSS class &lt;code&gt;sticky&lt;/code&gt; we transform the original header to the sticky one. Without JavaScript - this won't be possible.&lt;/p&gt;

&lt;p&gt;Maybe - so the idea - it is possible to have something set from the beginning. So the idea is to have the right layout and not rely on a scrolling-based transition.&lt;/p&gt;

&lt;p&gt;The starting point should be the CSS declaration &lt;code&gt;position: sticky&lt;/code&gt;. Using &lt;code&gt;position: sticky&lt;/code&gt; we can tell the browser to make the menu sticky (in relation to its scrolling container) without relying on a transition.&lt;/p&gt;

&lt;p&gt;The problem with &lt;code&gt;position: sticky&lt;/code&gt; is that we can only tell the browser what "position" we want to settle in. For instance, we can tell the browser &lt;code&gt;top: -10px&lt;/code&gt; to keep moving the header bar until it's 10 pixels inside the containers hidden area, i.e., shifted to the top by 10px. There is no way to add another shadow or other things we might be interested in...&lt;/p&gt;

&lt;p&gt;This is the point where we could call it a day, marking the task as "impossible" and move on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu063eb829fa3zkyaeuzz.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu063eb829fa3zkyaeuzz.jpg" alt="CSS one does not simply meme"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Luckily, there are still some tricks we can apply in this case. Let's look at what we want:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiysgn273ll6ey99u3qpp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiysgn273ll6ey99u3qpp.png" alt="Header transition"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In other words, we want to "reveal" part of the shadow when we move up. The moving up part we get covered by using &lt;code&gt;position: sticky&lt;/code&gt; as outlined - now the shadow part could be tricked in by really introducing such layers.&lt;/p&gt;

&lt;p&gt;What we mean specifically is that we could introduce two more "virtual" elements (using &lt;code&gt;::before&lt;/code&gt; and &lt;code&gt;::after&lt;/code&gt; pseudo selectors):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one element moving together with the sticky header (i.e., absolutely positioned) - initially "hiding" the shadow&lt;/li&gt;
&lt;li&gt;one element as a shadow that is being fixed; thus not moving with the sticky header (initially being hidden behind the absolutely positioned element)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that both need to be in the foreground. This means we have a layering such as:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9d502ownlcsj13xhmv3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb9d502ownlcsj13xhmv3.png" alt="Layering of elements"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real header (blue) is now shorter, but extended with the overlay element (green), which is initially hiding the shadow (purple). Once the page (red) scrolls, the header scrolls up to a certain point. Together with it the overlay is supposed to scroll, thus revealing the shadow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fclask9uidhs2colo8kx0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fclask9uidhs2colo8kx0.png" alt="Scrolling of elements"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In theory that should work - but in reality it will all be determined by the specific values we pick for the positioning of these elements.&lt;/p&gt;

&lt;p&gt;In CSS the following is needed to introduce the two virtual elements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="p"&gt;*::&lt;/span&gt;&lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;fixed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;%;&lt;/span&gt;
  &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;45&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;ccc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt; &lt;span class="nf"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;57&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;63&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;72&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9999&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="p"&gt;*::&lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;%;&lt;/span&gt;
  &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;55&lt;/span&gt;&lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;ccc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;9999&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;The header itself is positioned in a way that makes it be in front of these layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;z-index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&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;This creates the effect we are looking for - and without requiring any JavaScript whatsoever.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/vitejs-vite-ukluve?embed=1&amp;amp;file=src%2FPageHeader.jsx&amp;amp;view=editor" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Going to a full non-JS version of our header made the lighthouse score even better - being a full 100 on most / all pages. Now, whenever JS is needed it would be just there to enhance a certain view or give more details. There is no more JavaScript to actually enable navigation or other crucial means.&lt;/p&gt;

&lt;p&gt;As far as the sticky header is concerned, the shown trick only works in scenarios like ours. If you need more transformations then finding a good workaround might be more difficult. In the future that may change, but for now &lt;code&gt;position: sticky&lt;/code&gt; is quite limited and requires such tricks to also have scenarios such as ours covered.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>html</category>
      <category>css</category>
    </item>
  </channel>
</rss>
