<?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: Matti Bar-Zeev</title>
    <description>The latest articles on DEV Community by Matti Bar-Zeev (@mbarzeev).</description>
    <link>https://dev.to/mbarzeev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F210953%2F29b527cd-2c72-4a08-af7b-a8e4eb778637.png</url>
      <title>DEV Community: Matti Bar-Zeev</title>
      <link>https://dev.to/mbarzeev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mbarzeev"/>
    <language>en</language>
    <item>
      <title>Bazel For a Frontend Monorepo</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Sun, 05 Apr 2026 19:11:31 +0000</pubDate>
      <link>https://dev.to/mbarzeev/bazel-for-a-frontend-monorepo-da6</link>
      <guid>https://dev.to/mbarzeev/bazel-for-a-frontend-monorepo-da6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; This was done primarily as a study exercise. The goal was to learn Bazel hands-on and explore whether it could be a good fit for optimizing the build pipeline of this monorepo in the future. The &lt;code&gt;hooks&lt;/code&gt; package Bazelification covered here has landed in production by the time you’re reading this, but the full monorepo migration is still a future ambition, not a done deal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have a JavaScript monorepo called &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard&lt;/a&gt;. It holds a few React component libraries, hooks, linting plugins, and dev tooling. Nothing crazy, but it has been a good playground for experimenting with different build tools over the years.&lt;/p&gt;

&lt;p&gt;Right now, whenever a change lands in the repo, a GitHub Actions workflow kicks off and does roughly this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;pnpm install&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pnpm run build&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pnpm run test:since&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pnpm run lint:since&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And then it publishes any packages that need a new version.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;test:since&lt;/code&gt; and &lt;code&gt;lint:since&lt;/code&gt; scripts are smart enough to only run on packages that changed. But &lt;code&gt;pnpm install&lt;/code&gt; and &lt;code&gt;pnpm run build&lt;/code&gt; run unconditionally every time, on everything. That’s wasteful, and it was bugging me.&lt;/p&gt;

&lt;p&gt;I had been hearing about Bazel for a while. It promises smart caching, reproducible builds, and the ability to only rebuild what actually changed. So I figured: why not give it a shot on this monorepo? Worst case I learn something. Best case I have a clear path to a much faster CI pipeline.&lt;/p&gt;

&lt;p&gt;To be clear about scope, this post covers Bazelifying one package from the monorepo (the &lt;code&gt;hooks&lt;/code&gt; package) end to end: install, build, test, and lint. The goal was not to migrate the whole repo overnight, but to understand the full workflow for one package and figure out whether Bazel is worth the investment.&lt;/p&gt;

&lt;p&gt;What I was NOT trying to do here is model the internal dependency graph between packages in the monorepo (for example &lt;code&gt;components&lt;/code&gt; depending on &lt;code&gt;hooks&lt;/code&gt;). That is a next step and a separate story.&lt;/p&gt;

&lt;p&gt;Let’s get into it.&lt;/p&gt;

&lt;h1&gt;
  
  
  Caching
&lt;/h1&gt;

&lt;p&gt;Caching is a big part of what makes Bazel attractive. It can cache build artifacts locally or remotely, and in a CI context like GitHub Actions that remote cache is where the real speed gains come from. But setting that up properly is its own topic, and we’re not going to deal with it here. This post is about getting the fundamentals right first. That said, we will still benefit from local caching out of the box, which already means that re-running a build or test without any changes will be near-instant.&lt;/p&gt;

&lt;h1&gt;
  
  
  Preparing a project for Bazel
&lt;/h1&gt;

&lt;p&gt;Rather than installing Bazel directly, I’m going with Bazelisk. Think of it as the &lt;code&gt;.nvmrc&lt;/code&gt; equivalent for Bazel: it reads a &lt;code&gt;.bazelversion&lt;/code&gt; file and downloads the right binary for you. It also means your team won’t end up on different Bazel versions by accident.&lt;/p&gt;

&lt;p&gt;I install it at the root of the monorepo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; @bazel/bazelisk &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be able to invoke it via pnpm, I add a script to the root &lt;code&gt;package.json&lt;/code&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="err"&gt;“bazel”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“bazel”&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;code&gt;.bazelversion&lt;/code&gt; file at the project root pins the version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  The MODULE.bazel at the project’s root
&lt;/h1&gt;

&lt;p&gt;The project needs a root build file, for this we create the MODULE.bazel with this content for now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module(  
   name = "pedalboard",  
   version = "1.0.0"  
)  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Install NPM packages with Bazel
&lt;/h1&gt;

&lt;p&gt;Here is where Bazel throws its first curveball. When Bazel fetches npm packages, it does not put them in &lt;code&gt;node_modules&lt;/code&gt;. It downloads them into its own internal cache. My first reaction was “ok but every other tool in this repo expects &lt;code&gt;node_modules&lt;/code&gt; to exist, I’m not running two install systems side by side”. Turns out that concern is valid, and the solution is a rule that symlinks Bazel’s downloaded packages back into &lt;code&gt;node_modules&lt;/code&gt;. Fair enough.&lt;/p&gt;

&lt;p&gt;The ruleset for this is &lt;a href="https://github.com/aspect-build/rules_js" rel="noopener noreferrer"&gt;rules_js from aspect-build&lt;/a&gt;. I went through the &lt;a href="https://github.com/aspect-build/rules_js/blob/main/docs/pnpm.md" rel="noopener noreferrer"&gt;docs&lt;/a&gt; to get this right — AI was not helpful here at all, it kept giving me examples with mixed-up APIs from different versions.&lt;/p&gt;

&lt;p&gt;Here is the updated &lt;code&gt;MODULE.bazel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module(
   name = “pedalboard”,
   version = “1.0.0”
)

bazel_dep(name = “rules_nodejs”, version = “6.7.3”)
bazel_dep(name = “aspect_rules_js”, version = “3.0.3”)

node = use_extension(“@rules_nodejs//nodejs:extensions.bzl”, “node”)
node.toolchain(node_version_from_nvmrc = “//:.nvmrc”)
use_repo(node, “nodejs_toolchains”)

pnpm = use_extension(“@aspect_rules_js//npm:extensions.bzl”, “pnpm”)
use_repo(pnpm, “pnpm”)
pnpm.pnpm(pnpm_version_from = “//:package.json”)

npm = use_extension(“@aspect_rules_js//npm:extensions.bzl”, “npm”)
npm.npm_translate_lock(
   name = “npm”,
   pnpm_lock = “//:pnpm-lock.yaml”,
)
use_repo(npm, “npm”)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We pull in &lt;code&gt;rules_nodejs&lt;/code&gt; and &lt;code&gt;aspect_rules_js&lt;/code&gt; as dependencies, then use their extensions to pin the Node and pnpm versions. The &lt;code&gt;npm_translate_lock&lt;/code&gt; call reads &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; and makes all packages available as Bazel targets. This is, by the way, one of the reasons I &lt;a href="https://dev.to/mbarzeev/replacing-yarns-workspaces-with-pnpms-workspaces-5c1k"&gt;migrated from Yarn to pnpm&lt;/a&gt; — Bazel’s npm support is built around pnpm lockfiles. ;)&lt;/p&gt;

&lt;p&gt;We also need a &lt;code&gt;REPO.bazel&lt;/code&gt; file at the project root to tell Bazel to ignore &lt;code&gt;node_modules&lt;/code&gt; when scanning for source files (related to this &lt;a href="https://github.com/bazelbuild/bazel/issues/8106" rel="noopener noreferrer"&gt;open issue&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“””Repository configuration for the Pedalboard monorepo.”””

ignore_directories([“**/node_modules”])
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Bazelifying the hooks package
&lt;/h1&gt;

&lt;p&gt;The &lt;code&gt;hooks&lt;/code&gt; package is the one I’m starting with. It’s a straightforward TypeScript package — no SCSS, no complex bundling, just source files compiled to ESM and CJS, plus type declarations. A good first target.&lt;/p&gt;

&lt;p&gt;Since it’s TypeScript, I need a transpiler Bazel can work with. The recommended option is &lt;a href="https://registry.bazel.build/modules/aspect_rules_swc" rel="noopener noreferrer"&gt;aspect_rules_swc&lt;/a&gt; (&lt;a href="https://github.com/aspect-build/rules_swc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;), which wraps the SWC compiler. I add it to &lt;code&gt;MODULE.bazel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bazel_dep(name = “aspect_rules_swc”, version = “2.7.0”)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Compiling TypeScript
&lt;/h3&gt;

&lt;p&gt;The package currently builds two output formats: ESM and CJS. Each has its own SWC config. Here is &lt;code&gt;.swcrc.esm.json&lt;/code&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="err"&gt;“jsc”:&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="err"&gt;“parser”:&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="err"&gt;“syntax”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“typescript”&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="err"&gt;“tsx”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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="err"&gt;“target”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“es&lt;/span&gt;&lt;span class="mi"&gt;2020&lt;/span&gt;&lt;span class="err"&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="err"&gt;“module”:&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="err"&gt;“type”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“es&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&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="err"&gt;“sourceMaps”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;And &lt;code&gt;.swcrc.cjs.json&lt;/code&gt; is the same but with &lt;code&gt;”type”: “commonjs”&lt;/code&gt; in the module section.&lt;/p&gt;

&lt;p&gt;Now for the &lt;code&gt;BUILD.bazel&lt;/code&gt; file in the hooks package. The idea is to declare the source files as a &lt;code&gt;js_library&lt;/code&gt;, then pass them to two &lt;code&gt;swc&lt;/code&gt; rules — one for each output format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load(“@aspect_rules_swc//swc:defs.bzl”, “swc”)

filegroup(
   name = “build”,
   srcs = [
       “:compile_esm”,
       “:compile_cjs”,
   ],
)

js_library(
   name = “sources”,
   srcs = [“index.ts”] + glob([“src/**/*.ts”], exclude = [“**/*.test.ts”]),
)

swc(
   name = “compile_esm”,
   srcs = [“:sources”],
   swcrc = “.swcrc.esm.json”,
   source_maps = “true”,
   out_dir = “dist/esm”,
)

swc(
   name = “compile_cjs”,
   srcs = [“:sources”],
   swcrc = “.swcrc.cjs.json”,
   source_maps = “true”,
   out_dir = “dist/cjs”,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;pnpm bazel build :build&lt;/code&gt; from inside the package — Bazel compiles and drops the ESM and CJS artifacts into &lt;code&gt;bazel-bin&lt;/code&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.amazonaws.com%2Fuploads%2Farticles%2Fhz9hwf5qjbqhgi9yu5mf.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%2Fhz9hwf5qjbqhgi9yu5mf.png" alt=" " width="800" height="712"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not bad.&lt;/p&gt;

&lt;h3&gt;
  
  
  Type declarations
&lt;/h3&gt;

&lt;p&gt;SWC compiles TypeScript but does not emit &lt;code&gt;.d.ts&lt;/code&gt; files. For that I need &lt;code&gt;aspect_rules_ts&lt;/code&gt;, which wraps &lt;code&gt;tsc&lt;/code&gt; in declaration-only mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bazel_dep(name = “aspect_rules_ts”, version = “3.0.0”)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also add a flag to &lt;code&gt;.bazelrc&lt;/code&gt; so the rule respects &lt;code&gt;skipLibCheck&lt;/code&gt; from the tsconfig rather than overriding it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;common &lt;span class="nt"&gt;--&lt;/span&gt;@aspect_rules_ts//ts:skipLibCheck&lt;span class="o"&gt;=&lt;/span&gt;honor_tsconfig
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before wiring this into the hooks package, I need a root &lt;code&gt;BUILD.bazel&lt;/code&gt; file. Two things require it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aspect_rules_js&lt;/code&gt; needs the root to be a valid Bazel package so that &lt;code&gt;npm_translate_lock&lt;/code&gt; can resolve the &lt;code&gt;//:all&lt;/code&gt; target during module extension evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm_link_all_packages&lt;/code&gt; at the root is what child packages reference when they pull in node_modules. Without a root &lt;code&gt;BUILD.bazel&lt;/code&gt;, those targets simply don’t exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the root &lt;code&gt;BUILD.bazel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Root BUILD file required by aspect_rules_js

load(“@npm//:defs.bzl”, “npm_link_all_packages”)
load(“@aspect_rules_ts//ts:defs.bzl”, “ts_config”)

npm_link_all_packages(name = “node_modules”)

ts_config(
   name = “tsconfig_base”,
   src = “tsconfig.base.json”,
   visibility = [“//packages:__subpackages__”],
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I expose &lt;code&gt;tsconfig_base&lt;/code&gt; here because sub-packages extend it and need access to it from within Bazel’s sandbox.&lt;/p&gt;

&lt;p&gt;Back in the hooks &lt;code&gt;BUILD.bazel&lt;/code&gt;, I add the type compilation target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load(“@npm//:defs.bzl”, “npm_link_all_packages”)
...
load(“@aspect_rules_ts//ts:defs.bzl”, “ts_config”, “ts_project”)

npm_link_all_packages(name = “node_modules”)

filegroup(
   name = “build”,
   srcs = [
       “:compile_esm”,
       “:compile_cjs”,
       “:compile_types”, # Adding the step to compile types
   ],
)

# Defining the ts config for types, using the base config we defined earlier
ts_config(
   name = “tsconfig”,
   src = “tsconfig.esm.json”,
   deps = [“//:tsconfig_base”],
)

# Building it...
ts_project(
   name = “compile_types”,
   srcs = [“:sources”],
   tsconfig = “:tsconfig”,
   deps = [“:node_modules/@types/react”],
   declaration = True,
   declaration_dir = “dist/types”,
   emit_declaration_only = True,
   validate = False,
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of the deps is &lt;code&gt;@types/react&lt;/code&gt;, which we get through &lt;code&gt;npm_link_all_packages&lt;/code&gt;. Here is where we’re at 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.amazonaws.com%2Fuploads%2Farticles%2Faxjypi44edouh4xut48u.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%2Faxjypi44edouh4xut48u.png" alt=" " width="721" height="1482"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One gotcha I hit: Bazel does not like the &lt;code&gt;v&lt;/code&gt; prefix in &lt;code&gt;.nvmrc&lt;/code&gt; (as in &lt;code&gt;v20.0.0&lt;/code&gt;). It needs the bare version number. Not the most helpful error message, but ok.&lt;/p&gt;

&lt;h3&gt;
  
  
  Copying the files to the package’s dist dir
&lt;/h3&gt;

&lt;p&gt;Bazel writes all build outputs to &lt;code&gt;bazel-bin&lt;/code&gt;, not back to the source tree. That’s by design. But our publishing tool (Changesets) expects the &lt;code&gt;dist&lt;/code&gt; directory to sit inside the package itself. So I need to copy the artifacts back.&lt;/p&gt;

&lt;p&gt;For that I add &lt;code&gt;aspect_bazel_lib&lt;/code&gt; to &lt;code&gt;MODULE.bazel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bazel_dep(name = “aspect_bazel_lib”, version = “2.9.4”)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the hooks &lt;code&gt;BUILD.bazel&lt;/code&gt;, two new targets handle the copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copy_to_directory(
   name = “dist_dir”,
   srcs = [
       “:compile_esm”,
       “:compile_cjs”,
       “:compile_types”,
   ],
   replace_prefixes = {“dist/”: “”},
)

write_source_files(
   name = “build_and_copy”,
   files = {
       “dist”: “:dist_dir”,
   },
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;copy_to_directory&lt;/code&gt; collects the outputs and strips the &lt;code&gt;dist/&lt;/code&gt; prefix so we end up with &lt;code&gt;esm/&lt;/code&gt;, &lt;code&gt;cjs/&lt;/code&gt;, and &lt;code&gt;types/&lt;/code&gt; at the top level. &lt;code&gt;write_source_files&lt;/code&gt; then copies that directory back into the source tree.&lt;/p&gt;

&lt;p&gt;Running the &lt;code&gt;build_and_copy&lt;/code&gt; target triggers the full build and writes the results to &lt;code&gt;dist/&lt;/code&gt;. The last piece is updating the &lt;code&gt;build&lt;/code&gt; npm script in &lt;code&gt;package.json&lt;/code&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="err"&gt;“build”:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;“bazel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;run&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//packages/hooks:build_and_copy”&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build done. On to testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Running Jest
&lt;/h3&gt;

&lt;p&gt;Our test runner is Jest, so for that we need to install the aspect_rules_jest for Bazel. In the MODULE.bazel we add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bazel_dep(name = "aspect_rules_jest", version = "0.25.2")  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the hookd BUILD.bazel file we load the rule&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@aspect_rules_jest//jest:defs.bzl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jest_test&lt;/span&gt;&lt;span class="sh"&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 Rule requires the project to have “jest-cli” as a dependency. So we need to add it to the package.json of the project’s root.&lt;/p&gt;

&lt;p&gt;The jest configuration we’re using also inherits from a root jest config, and we need to expose it to nested packages. Since the tests in Bazel run its sandbox (bazel-bin) we need to expose this root configuration so it will be available there. This is why we’re using js_libray for it. In the root’s BUILD.bazel we add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...  
load("@aspect_rules_js//js:defs.bzl", "js_library")

...

js_library(  
   name = "jest_config_base",  
   srcs = ["jest.config.base.js"],  
   visibility = ["//packages:__subpackages__"],  
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last we set the target in the hooks package BUILD.bazel file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...  
load("@aspect_rules_jest//jest:defs.bzl", "jest_test")

...

js_library(  
   name = "test_sources",  
   srcs = glob(["src/**/*.test.ts"]),  
)

...

jest_test(  
   name = "jest",  
   config = "jest.config.js",  
   auto_configure_reporters = False,  
   args = ["--reporters=default"],  
   data = [  
       ":sources",  
       ":test_sources",  
       "jest.config.js",  
       "//:jest_config_base",  
       "//:node_modules/@swc/core",  
       "//:node_modules/@swc/jest",  
       ":node_modules/@testing-library/react-hooks",  
       ":node_modules/react",  
   ],  
   node_modules = ":node_modules",  
)  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in order to get a logs which are not riddled with a lot of unescaped color chars, you can add this to your &lt;code&gt;.bazelrc&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;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--test_env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;FORCE_COLOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last thing remaining to do is replace the package.json test npm script to call bazel, like so:&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="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bazel run //packages/hooks:jest"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Coverage report
&lt;/h3&gt;

&lt;p&gt;It’s possible to pass parameters to Bazel, meaning that you run a npm script, and it passes the params to Bazel target. For that you need to add an “extra” 2 “--”, one to pass the Bazel and the other to pass to Jest. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--coverage&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can make it a bit easier if we set the npm script to include the first “--”:&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="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bazel run //packages/hooks:jest --"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can call it like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--coverage&lt;/span&gt;  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also helps when we call it from the root of the project when we collect the coverage for all. In that case pnpm knows to pass the param without needing the extra “--”, so nothing has to be changed on the root’s package.json&lt;/p&gt;

&lt;p&gt;What we have will trigger the coverage report, but you won’t see the coverage directory in the package, since Bazel writes it into the sandbox. This is not what we want.&lt;br&gt;&lt;br&gt;
So you can tell Bazel test target where you want the coverage dir to be written in, using the coverageDirectory param. Sadly, it does not respect the Jest config for this, and you need to put it in the actual call. So we end up with 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="nl"&gt;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bazel run //packages/hooks:jest -- --coverageDirectory=$(pwd)/coverage"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Coverage paths and the NYC reporter
&lt;/h3&gt;

&lt;p&gt;There is one more wrinkle with coverage. The root's &lt;code&gt;coverage:combined&lt;/code&gt; script collects each package's &lt;code&gt;coverage-final.json&lt;/code&gt; into &lt;code&gt;.nyc_output/&lt;/code&gt; using the &lt;code&gt;collectFiles&lt;/code&gt; script from &lt;code&gt;@pedalboard/scripts&lt;/code&gt;, then runs &lt;code&gt;nyc report&lt;/code&gt; to produce the final lcov output.&lt;/p&gt;

&lt;p&gt;The problem: when Jest runs inside Bazel's sandbox, the file paths it embeds in &lt;code&gt;coverage-final.json&lt;/code&gt; point deep into the Bazel cache. NYC tries to open each file at that exact path to annotate the source, but those sandbox paths are gone by the time the report runs. The coverage data is all there — statements, branches, functions — but the reporter can't find the source files and falls over.&lt;/p&gt;

&lt;p&gt;The fix is to remap the paths at collection time. Instead of copying each &lt;code&gt;coverage-final.json&lt;/code&gt; straight into &lt;code&gt;.nyc_output/&lt;/code&gt;, the &lt;code&gt;collectFiles&lt;/code&gt; script now normalizes every path in the JSON back to its real relative path from the repo root before writing the file. NYC can then resolve the sources without any extra configuration, regardless of whether the coverage was produced by Bazel or plain Jest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch mode
&lt;/h3&gt;

&lt;p&gt;At the current state the aspect_rules_jest does not support the watch mode for Jest and to make it work in Bazel requires additional configuration, which I think is not necessary. Watch mode is for dev time, and I think it is perfectly fine to use the plain old npm Jest to do that.&lt;br&gt;&lt;br&gt;
For that I create another npm script:&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="nl"&gt;"test:watch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jest --watch"&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there we have it.&lt;br&gt;&lt;br&gt;
Cool ! we’re ready to jump to the linting process&lt;/p&gt;
&lt;h2&gt;
  
  
  Linting
&lt;/h2&gt;

&lt;p&gt;We’re going to go to the Bazel center registry again to look for a rule that can help us. This is the rule we need to add to the MODULE.bazel file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bazel_dep(name = "aspect_rules_lint", version = "2.3.0")  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a version conflict with the rules_go dependency, and to solve that we override its version, like this in the MODULE.bazel file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;single_version_override(  
   module_name = "rules_go",  
   version = "0.60.0",  
)  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Exposing the ESlint config for all sub packages
&lt;/h3&gt;

&lt;p&gt;In the root’s BUILD.bazel file we will make the eslint configuration available for all sub packages. We are using the js_library for that, and notice that we add all the dependencies that this configuration requires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;js_library(  
   name = "eslintrc",  
   srcs = ["eslint.config.mjs"],  
   deps = [  
       ":node_modules/globals",  
       ":node_modules/eslint",  
       ":node_modules/@eslint/js",  
       ":node_modules/eslint-plugin-react",  
       ":node_modules/typescript-eslint",  
       ":node_modules/@typescript-eslint/parser",  
       ":node_modules/@typescript-eslint/eslint-plugin",  
       ":node_modules/@pedalboard/eslint-plugin-craftsmanlint",  
   ],  
   visibility = ["//packages:__subpackages__"],  
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Making ESlint available for all sub packages
&lt;/h3&gt;

&lt;p&gt;The recommended way to go about linting in Bazel is to create a dedicated tools/lint “package” which makes the ESlint tool available to all packages. For that we create a tools/lint directory and put 2 files in it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tools/lint/BUILD.bazel&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load("@npm//:eslint/package_json.bzl", eslint_bin = "bin")

eslint_bin.eslint_binary(  
   name = "eslint",  
   data = [  
       "//:node_modules/chalk",  
   ],  
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This declares the eslint_binary target (//tools/lint:eslint) that the aspect uses to actually run ESLint&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;tools/lint/linterz.bzl&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load("@aspect_rules_lint//lint:eslint.bzl", "lint_eslint_aspect")  
load("@aspect_rules_lint//lint:lint_test.bzl", "lint_test")

eslint = lint_eslint_aspect(  
   binary = Label("//tools/lint:eslint"),  
   configs = [  
       Label("//:eslintrc"),  
   ],  
)

eslint_test = lint_test(aspect = eslint)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a Starlark library that defines and exports the eslint aspect and eslint_test factory for sub packages to load()&lt;br&gt;&lt;br&gt;
Here we use the lint_eslint_aspect rule and notice that we give it the configuration we declared recently in the root’s BUILD.bazel file.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using the eslint_test target
&lt;/h3&gt;

&lt;p&gt;Now that we have all this set we can use the eslint_test target. We do that in the package’s BUILD.bazel file:&lt;br&gt;
&lt;/p&gt;

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

load("@aspect_rules_js//js:defs.bzl", "js_library")  
load("//tools/lint:linters.bzl", "eslint_test")

...

eslint_test(  
   name = "lint",  
   timeout = "short",  
   srcs = [  
       ":compile_types",  # ts_project — all non-test TS sources  
       ":test_sources",   # js_library — test files  
   ],  
)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last thing remaining to do is replace the package.json test npm script to call bazel, like so:&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="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bazel run //packages/hooks:lint"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;So what did we actually accomplish here?&lt;/p&gt;

&lt;p&gt;We took one package from a JavaScript monorepo and plugged it into Bazel end to end: dependency installation via the pnpm lockfile, TypeScript compilation to ESM and CJS, type declaration generation, copying build artifacts back to &lt;code&gt;dist/&lt;/code&gt;, running Jest tests with coverage support, and ESLint linting. All through Bazel targets wired up to the existing npm scripts, so nothing in the repo's external interface changed.&lt;/p&gt;

&lt;p&gt;Was it worth it? That depends on what you're optimizing for. The setup cost is real — Bazel is not exactly beginner-friendly, and I spent more time than I'd like to admit figuring out why things were failing. The error messages are unhelpful and the documentation has gaps that AI tools made worse, not better.&lt;/p&gt;

&lt;p&gt;But the fundamentals are solid. The local caching already pays off during development, and the path to remote caching in CI is clear. Once the dependency graph between packages is modeled in Bazel (still to come), the build system will know exactly what to rebuild and what to skip. That's the goal.&lt;/p&gt;

&lt;p&gt;For now, one package is Bazelified and in production. The rest of the monorepo will follow — eventually. No promises on timeline. 😄&lt;/p&gt;

&lt;p&gt;You can find the full code in the &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard repo on GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>bazel</category>
      <category>monorepo</category>
      <category>typescript</category>
      <category>javascript</category>
    </item>
    <item>
      <title>From Lerna to Changesets</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Sun, 15 Mar 2026 14:13:26 +0000</pubDate>
      <link>https://dev.to/mbarzeev/from-lerna-to-changesets-4cip</link>
      <guid>https://dev.to/mbarzeev/from-lerna-to-changesets-4cip</guid>
      <description>&lt;p&gt;If you've been following the saga of my &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard&lt;/a&gt; monorepo, you'll know that Lerna and I have a... complicated relationship. I originally reached for it in my &lt;a href="https://dev.to/mbarzeev/no-bs-monorepo-part-1-3c3a"&gt;No BS monorepo&lt;/a&gt; series to handle the two things monorepos constantly need: versioning and publishing. It worked, then it &lt;a href="https://dev.to/mbarzeev/lerna-is-no-longer-maintained-now-what-part-1-4kd6"&gt;stopped being maintained&lt;/a&gt;, then it &lt;a href="https://dev.to/mbarzeev/this-is-no-lerna-its-a-freaking-phoenix-1cii"&gt;rose from the ashes as a phoenix&lt;/a&gt;, and here we are - still using it. But lately I've been looking at &lt;a href="https://github.com/changesets/changesets" rel="noopener noreferrer"&gt;Changesets&lt;/a&gt; and wondering if it's time to finally make the switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Lerna Actually Does in Pedalboard (at This Point)
&lt;/h2&gt;

&lt;p&gt;This is worth being honest about, because Lerna's scope in this project has been narrowing for a while.&lt;br&gt;
What's left for Lerna is captured in two blocks of &lt;code&gt;lerna.json&lt;/code&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;"npmClient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"useNx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&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"&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;"ignoreChanges"&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="s2"&gt;"ignored-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.md"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore(release): publish %s"&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;"version"&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore(release): version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"allowBranch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"conventionalCommits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;span class="nl"&gt;"packages"&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="s2"&gt;"packages/*"&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;"independent"&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;That's it. Lerna's entire job, at this point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Detect which packages changed&lt;/strong&gt; since the last release&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bump their versions&lt;/strong&gt; based on conventional commit messages (patch/minor/major)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tag the release&lt;/strong&gt; in git&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish to npm&lt;/strong&gt; via &lt;code&gt;lerna publish --yes --no-verify-access&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The root &lt;code&gt;package.json&lt;/code&gt; confirms this - the only mention of &lt;code&gt;lerna&lt;/code&gt; is a single script:&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="nl"&gt;"publish:lerna"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lerna publish --yes --no-verify-access"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fully-grown monorepo tool, reduced to a very specific, yet very important, job: release management.&lt;/p&gt;

&lt;p&gt;I wrote about this philosophical evolution in &lt;a href="https://dev.to/mbarzeev/rethinking-the-one-ring-to-rule-them-all-monorepo-manager-2n5g"&gt;Rethinking the "One Ring To Rule Them all" Monorepo manager&lt;/a&gt;, the idea that one tool shouldn't do everything - each tool should own its lane. Lerna owns the release lane. But is it the &lt;em&gt;best&lt;/em&gt; tool for that lane?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Changesets
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/changesets/changesets" rel="noopener noreferrer"&gt;Changesets&lt;/a&gt; (technically &lt;code&gt;@changesets/cli&lt;/code&gt;) takes a different approach to the same problem. Instead of reading conventional commit messages and auto-detecting what changed, it asks developers to be &lt;em&gt;intentional&lt;/em&gt;: when you make a change, you run &lt;code&gt;changeset&lt;/code&gt; and write a brief description of what changed and what kind of semver bump it warrants (patch, minor, major). That creates a small markdown file in a &lt;code&gt;.changeset/&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;When it's time to release, you run &lt;code&gt;changeset version&lt;/code&gt; to consume those files and bump the appropriate package versions, then &lt;code&gt;changeset publish&lt;/code&gt; to push to npm.&lt;/p&gt;

&lt;p&gt;The appeal, for me, is the &lt;strong&gt;explicitness&lt;/strong&gt;. With Lerna's conventional commits approach, version bumps are inferred from commit messages - which means they're only as reliable as your commit discipline. If someone sneaks a breaking change in with a &lt;code&gt;fix:&lt;/code&gt; prefix (we've all been there, don't lie), Lerna will happily publish a patch. Changesets sidesteps this by making the bump intent part of the PR itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initializing Changesets
&lt;/h2&gt;

&lt;p&gt;Enough theory. The initialization is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dlx @changesets/cli init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;.changeset/&lt;/code&gt; directory with a &lt;code&gt;README.md&lt;/code&gt; and, more importantly, a &lt;code&gt;config.json&lt;/code&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://unpkg.com/@changesets/config@3.1.3/schema.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"changelog"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@changesets/cli/changelog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"commit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fixed"&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;"linked"&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;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"restricted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"baseBranch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updateInternalDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"patch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ignore"&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="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;Not bad for a starting point, but two things need fixing immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two config issues, right off the bat
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;baseBranch: "main"&lt;/code&gt;&lt;/strong&gt; - Pedalboard's branch is &lt;code&gt;master&lt;/code&gt;, not &lt;code&gt;main&lt;/code&gt;. Changesets uses this to determine what's changed since the last release. Wrong base means &lt;code&gt;changeset status&lt;/code&gt; compares against the wrong thing:&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="nl"&gt;"baseBranch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"master"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;access: "restricted"&lt;/code&gt;&lt;/strong&gt; - This controls the npm access level. "restricted" means private packages, which is a sensible safe default, but all of Pedalboard's packages are public on npm:&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="nl"&gt;"access"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"public"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines. Two things I'm glad I noticed before pushing a release and wondering why nothing published.&lt;/p&gt;

&lt;p&gt;The other defaults are fine: &lt;code&gt;"commit": false&lt;/code&gt; means Changesets won't auto-commit the version bumps (CI handles that), and &lt;code&gt;"updateInternalDependencies": "patch"&lt;/code&gt; means when a package is bumped, any internal packages that depend on it get a patch bump too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Good Time to Clean Up Dead Scripts
&lt;/h2&gt;

&lt;p&gt;While looking at what Lerna does, I also noticed a &lt;code&gt;publish:lerna:skip-git&lt;/code&gt; script sitting in &lt;code&gt;package.json&lt;/code&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="nl"&gt;"publish:lerna:skip-git"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lerna publish --yes --no-verify-access --no-git-tag-version --no-push --loglevel=silly"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flags tell the story - &lt;code&gt;--no-git-tag-version --no-push&lt;/code&gt; were there to let you test publishing locally without polluting git history. A debug escape hatch.&lt;/p&gt;

&lt;p&gt;A quick search across the repo and all the blog posts confirmed it: &lt;strong&gt;nobody ever called it&lt;/strong&gt;. Not the GitHub Actions workflow, not any other script, not a single post. It was just sitting there.&lt;/p&gt;

&lt;p&gt;I considered whether to create a Changesets equivalent, but Changesets already separates versioning and publishing into two distinct commands, and with &lt;code&gt;"commit": false&lt;/code&gt; in the config, &lt;code&gt;changeset version&lt;/code&gt; doesn't touch git at all. If I ever need a no-tags publish, &lt;code&gt;changeset publish --no-git-tag&lt;/code&gt; is one command away. No need to enshrine it as a named script until it earns its place.&lt;/p&gt;

&lt;p&gt;Deleted. One less thing to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling Out Lerna Completely
&lt;/h2&gt;

&lt;p&gt;With the scripts gone, time to finish the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remove the &lt;code&gt;lerna&lt;/code&gt; devDependency:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm remove lerna &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That uninstalls it from the workspace root and cleans up &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;. Satisfying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete &lt;code&gt;lerna.json&lt;/code&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The config file that's been in this repo since the very beginning - independent versioning, conventional commits, &lt;code&gt;allowBranch: master&lt;/code&gt; - none of it is relevant anymore. Deleted.&lt;/p&gt;

&lt;p&gt;At this point, Lerna is fully gone from the project. No dependency, no config, no scripts. The repo doesn't know it ever existed. Changesets now owns the release lane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Changesets Properly
&lt;/h2&gt;

&lt;p&gt;Using &lt;code&gt;pnpm dlx&lt;/code&gt; to run Changesets is fine for a quick init, but not something you want in CI or for contributors who clone the repo. Time to make it a first-class devDependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @changesets/cli &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-w&lt;/code&gt; flag installs it at the workspace root, making it available across all packages. With it installed, the &lt;code&gt;changeset&lt;/code&gt; binary lands in &lt;code&gt;node_modules/.bin&lt;/code&gt;, so &lt;code&gt;pnpm changeset&lt;/code&gt; just works.&lt;/p&gt;

&lt;p&gt;For the versioning step, I added a named script - something that reads clearly in CI and in conversation. I considered just &lt;code&gt;version&lt;/code&gt;, but that's a reserved npm lifecycle hook that runs before &lt;code&gt;pnpm version&lt;/code&gt;. &lt;code&gt;version:changeset&lt;/code&gt; is explicit and unambiguous:&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="nl"&gt;"version:changeset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changeset version"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;pnpm run version:changeset&lt;/code&gt; consumes all pending changeset files, bumps the affected package versions, and updates the CHANGELOGs. And I mean &lt;em&gt;consume&lt;/em&gt; - the changeset files are deleted as part of the process. They're a one-time intent, not a log. Once applied, they're gone. If the changeset files aren't committed to git before you run this, there's no way to get them back. Lesson learned the slightly painful way.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Mindset Shift: Intent Over Detection
&lt;/h2&gt;

&lt;p&gt;This is where Changesets genuinely thinks differently from Lerna, and it took me a moment to internalize.&lt;/p&gt;

&lt;p&gt;With Lerna's conventional commits approach, version bumps were &lt;em&gt;detected&lt;/em&gt; - Lerna would look at what changed in git and infer a bump from the commit message. Automatic. You made commits, Lerna figured out the rest.&lt;/p&gt;

&lt;p&gt;Changesets doesn't detect anything. It only knows what you &lt;em&gt;tell it&lt;/em&gt;. When you run &lt;code&gt;pnpm changeset&lt;/code&gt;, it asks you to explicitly select which packages are affected and what kind of bump they warrant. No git inspection, no inference. Pure intent.&lt;/p&gt;

&lt;p&gt;The practical consequence: &lt;strong&gt;infra-only changes don't need a changeset at all.&lt;/strong&gt; This whole migration, removing Lerna, adding Changesets, updating scripts, touched the root &lt;code&gt;package.json&lt;/code&gt; and the CI config. None of the published packages changed. No new features, no bug fixes, nothing that affects consumers. So no changeset is needed, and no packages will be bumped. That's correct behaviour.&lt;/p&gt;

&lt;p&gt;This is very different from Lerna, where a commit like &lt;code&gt;chore: migrate from Lerna to Changesets&lt;/code&gt; touching the root would still be scanned and might trigger bumps depending on how it interpreted the scope.&lt;/p&gt;

&lt;p&gt;The flip side is the cascade effect. When you &lt;em&gt;do&lt;/em&gt; create a changeset for a package that others depend on, Changesets will automatically bump those dependents too - controlled by &lt;code&gt;"updateInternalDependencies": "patch"&lt;/code&gt; in the config. So if &lt;code&gt;@pedalboard/hooks&lt;/code&gt; gets a minor bump, &lt;code&gt;@pedalboard/components&lt;/code&gt; (which depends on it) gets a patch bump to update its dependency range. It cascades through the graph.&lt;/p&gt;

&lt;p&gt;Explicit intent in, correct cascade out. Once you accept that the changeset file &lt;em&gt;is&lt;/em&gt; the release decision - not a side effect of a commit - the whole model clicks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Publish Script
&lt;/h2&gt;

&lt;p&gt;With versioning covered, publishing needs its own script:&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="nl"&gt;"publish:changeset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"changeset publish"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;changeset publish&lt;/code&gt; does three things in sequence: checks which packages have a version that hasn't been published to npm yet, publishes those, and creates and pushes a git tag for each one (e.g. &lt;code&gt;@pedalboard/hooks@1.2.3&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;That last part, the git tag push, means the CI runner needs a git identity configured before this script runs. The existing workflow already has this from the Lerna days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;git config --local user.name 'github-actions[bot]'&lt;/span&gt;
    &lt;span class="s"&gt;git config --local user.email 'github-actions[bot]@users.noreply.github.com'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That step stays. Changesets needs it for the same reason Lerna did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforcing the Changeset Discipline in CI
&lt;/h2&gt;

&lt;p&gt;One thing Lerna's conventional commits approach gave for free: it simply wouldn't bump a package if no relevant commit touched it. With Changesets, the responsibility is on the developer. Which raises a fair question: what stops someone from opening a PR that modifies a package and forgetting to create a changeset?&lt;/p&gt;

&lt;p&gt;Changesets has an answer: &lt;code&gt;changeset status --since=origin/master&lt;/code&gt; exits non-zero if any packages have changed without a covering changeset file. You can wire it into CI as a gate that fails early, before lint and tests even run. For now I'm leaving it out - I will take care of it in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating the GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;With all the pieces in place, the final step is the workflow itself. The complete updated &lt;code&gt;npm-publish.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and publish&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
    &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;types&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
        &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
              &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
              &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run lint:since&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run test:since&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run build&lt;/span&gt;

            &lt;span class="c1"&gt;# Publish to NPM&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
              &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
                  &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://registry.npmjs.org/&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                  &lt;span class="s"&gt;git config --local user.name 'github-actions[bot]'&lt;/span&gt;
                  &lt;span class="s"&gt;git config --local user.email 'github-actions[bot]@users.noreply.github.com'&lt;/span&gt;
            &lt;span class="c1"&gt;# Don't run custom Git hooks&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git config --local core.hooksPath .git/hooks&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run version:changeset&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;git add -A&lt;/span&gt;
            &lt;span class="c1"&gt;# Skip commit and push if version:changeset produced no changes (e.g. no pending changesets)&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
                  &lt;span class="s"&gt;git diff --staged --quiet || (git commit -m "chore(release): version" &amp;amp;&amp;amp; git push)&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run publish:changeset&lt;/span&gt;
              &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.npm_token }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting in the publish sequence. &lt;code&gt;version:changeset&lt;/code&gt; must run &lt;em&gt;before&lt;/em&gt; &lt;code&gt;publish:changeset&lt;/code&gt; - it's what consumes the changeset files, bumps &lt;code&gt;package.json&lt;/code&gt; versions, and updates the CHANGELOGs. Without it, &lt;code&gt;changeset publish&lt;/code&gt; has nothing new to publish (it only publishes versions not yet on npm).&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;version:changeset&lt;/code&gt; modifies files, those changes need to be committed and pushed back to the repo before publishing. The single &lt;code&gt;run&lt;/code&gt; step handles both: &lt;code&gt;git diff --staged --quiet&lt;/code&gt; exits zero if nothing changed, short-circuiting the rest - so commit and push are skipped entirely if there were no pending changesets. If there &lt;em&gt;were&lt;/em&gt; changes, it commits and pushes in one go. The &lt;code&gt;|&lt;/code&gt; block scalar is needed because YAML would misinterpret the &lt;code&gt;||&lt;/code&gt; shell operator as a nested mapping otherwise.&lt;/p&gt;

&lt;p&gt;Everything else - the git identity config, the hook bypass, the &lt;code&gt;fetch-depth: 0&lt;/code&gt; for full git history, the master-only trigger - carries over unchanged. Lerna is gone, Changesets is in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This migration started as a question - "is Lerna still the right tool for this one job?" - and ended with a cleaner, more intentional release flow.&lt;/p&gt;

&lt;p&gt;Here's what changed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lerna&lt;/code&gt; and &lt;code&gt;lerna.json&lt;/code&gt; are gone&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@changesets/cli&lt;/code&gt; is a proper devDependency&lt;/li&gt;
&lt;li&gt;Two new scripts in &lt;code&gt;package.json&lt;/code&gt;: &lt;code&gt;version:changeset&lt;/code&gt;, &lt;code&gt;publish:changeset&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;One line changed in the GitHub Actions workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But the bigger takeaway is the mental model shift. Lerna inferred release intent from git history. Changesets makes you declare it explicitly. That sounds like more work, and honestly - it is, slightly. But it's the &lt;em&gt;right&lt;/em&gt; work. The changeset file becomes part of your PR, reviewed alongside the code, describing the user-facing impact. The version bump is a deliberate decision, not an inference.&lt;/p&gt;

&lt;p&gt;Would I recommend this migration for every monorepo? If your release discipline is solid and conventional commits work for you, Lerna (or its successors) might be fine. But if you want version bumps to be explicit, reviewable, and decoupled from commit message conventions - Changesets is worth the switch.&lt;/p&gt;

&lt;p&gt;As always, the full code is in the &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard repository&lt;/a&gt; on GitHub. Have you made this migration yourself, or are you sticking with Lerna? I'd love to hear how others are handling it.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Related posts:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mbarzeev/no-bs-monorepo-part-1-3c3a"&gt;No BS monorepo - Part 1&lt;/a&gt; - where Lerna first entered Pedalboard&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mbarzeev/no-bs-monorepo-part-2-3im2"&gt;No BS monorepo - Part 2&lt;/a&gt; - auto-publishing with Lerna and GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mbarzeev/lerna-is-no-longer-maintained-now-what-part-1-4kd6"&gt;Lerna is no longer maintained. Now what? - Part 1&lt;/a&gt; - the NX detour&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mbarzeev/this-is-no-lerna-its-a-freaking-phoenix-1cii"&gt;This is no Lerna, it's a freaking Phoenix!&lt;/a&gt; - the rant that aged well&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mbarzeev/rethinking-the-one-ring-to-rule-them-all-monorepo-manager-2n5g"&gt;Rethinking the "One Ring To Rule Them all" Monorepo manager&lt;/a&gt; - why each tool should own its lane&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/mbarzeev/migrating-a-monorepo-from-yarn-to-pnpm-45cn"&gt;Migrating a monorepo from Yarn to PNPM&lt;/a&gt; - the previous leg of this journey&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>npm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Migrating a monorepo from Yarn to PNPM</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Sat, 07 Mar 2026 14:05:04 +0000</pubDate>
      <link>https://dev.to/mbarzeev/migrating-a-monorepo-from-yarn-to-pnpm-45cn</link>
      <guid>https://dev.to/mbarzeev/migrating-a-monorepo-from-yarn-to-pnpm-45cn</guid>
      <description>&lt;p&gt;My &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard&lt;/a&gt; monorepo has been running on Yarn 3 workspaces for quite a while now. I've written about it &lt;a href="https://dev.to/mbarzeev/no-bs-monorepo-part-1-3c3a"&gt;many&lt;/a&gt; &lt;a href="https://dev.to/mbarzeev/no-bs-monorepo-part-2-3im2"&gt;times&lt;/a&gt;, setting it up, refactoring &lt;a href="https://dev.to/mbarzeev/yarn-workspace-scripts-refactor-a-case-study-2f25"&gt;workspace scripts&lt;/a&gt;, making it &lt;a href="https://dev.to/mbarzeev/build-just-what-you-need-32kd"&gt;build just what's needed&lt;/a&gt;, and even going through a whole &lt;a href="https://dev.to/mbarzeev/this-is-no-lerna-its-a-freaking-phoenix-1cii"&gt;Lerna drama&lt;/a&gt;. It's been a journey, and Yarn has served me well.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Author's note (March 2026):&lt;/strong&gt; Since this post was written, this monorepo has migrated from Lerna to Changesets. Some of the workflows, scripts, and tooling described here have changed. See &lt;a href="https://dev.to/mbarzeev/from-lerna-to-changesets-4cip"&gt;From Lerna to Changesets&lt;/a&gt; for what's different&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But here's the thing, the JavaScript ecosystem moves fast, and pnpm has been gaining serious traction. Its strict dependency handling, blazing speed, and disk-space efficiency are hard to ignore. So I figured it's time to rip off the band-aid and migrate my workspaces from Yarn to pnpm.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through the actual migration process, the gotchas I ran into, and whether pnpm lives up to the hype for a real-world monorepo.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Current Setup
&lt;/h2&gt;

&lt;p&gt;Before we start tearing things apart, let's look at what we're working with. The Pedalboard monorepo currently runs on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Yarn 3.2.0&lt;/strong&gt; with the workspace-tools plugin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lerna&lt;/strong&gt; (independent versioning) for publishing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;7 packages&lt;/strong&gt; under &lt;code&gt;packages/*&lt;/code&gt; - React components, hooks, linting plugins, and dev tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the root &lt;code&gt;package.json&lt;/code&gt; workspace configuration:&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;"workspaces"&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="s2"&gt;"packages/*"&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;"packageManager"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn@3.2.0"&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;And the &lt;code&gt;.yarnrc.yml&lt;/code&gt; that ties it all together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;nodeLinker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node-modules&lt;/span&gt;

&lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@yarnpkg/plugin-workspace-tools"&lt;/span&gt;

&lt;span class="na"&gt;yarnPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.yarn/releases/yarn-3.2.0.cjs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workspace scripts use Yarn's &lt;code&gt;workspaces foreach&lt;/code&gt; command for running tasks across packages:&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;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn workspaces foreach -pRv run test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn workspaces foreach -pRv run lint"&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;"yarn workspaces foreach -ptv run build"&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;So there's Yarn-specific syntax sprinkled throughout. Let's see what it takes to swap it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Out With the Old
&lt;/h2&gt;

&lt;p&gt;The first step is satisfyingly destructive, deleting all the Yarn-specific files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.yarnrc.yml&lt;/code&gt; - gone&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.yarn/&lt;/code&gt; directory (vendored binary, plugins, cache) - gone&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;yarn.lock&lt;/code&gt; - gone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In their place, pnpm needs just one config file. I created a &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; at the root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;packages/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No plugins, no vendored binary, no special config. Already feeling lighter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translating the Scripts
&lt;/h2&gt;

&lt;p&gt;This is where the meat of the migration lives. Yarn and pnpm have different CLI syntax for workspace operations, so every script that touches workspaces needs updating.&lt;/p&gt;

&lt;p&gt;The root &lt;code&gt;package.json&lt;/code&gt; scripts went from 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;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn workspaces foreach -pRv run test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:since"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn workspaces foreach --since -pRv run test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn workspaces foreach -pRv run lint"&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;"yarn workspaces foreach -ptv run build"&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;To 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;"test"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r run test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"test:since"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm --filter '...[origin/master]' run test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r run lint"&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;"pnpm -r run build"&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;pnpm -r&lt;/code&gt; flag handles recursive execution across packages. But the interesting bit is the &lt;code&gt;--since&lt;/code&gt; replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  How pnpm Handles "Since"
&lt;/h3&gt;

&lt;p&gt;Yarn's &lt;code&gt;--since&lt;/code&gt; flag runs commands only on packages that changed compared to the default branch. pnpm achieves this with &lt;code&gt;--filter&lt;/code&gt; using a git-diff syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s1"&gt;'...[origin/master]'&lt;/span&gt; run &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[origin/master]&lt;/code&gt; part selects packages where files changed compared to &lt;code&gt;origin/master&lt;/code&gt;. The &lt;code&gt;...&lt;/code&gt; prefix is the clever bit - it means "also include dependents of changed packages." So if &lt;code&gt;@pedalboard/hooks&lt;/code&gt; changed, &lt;code&gt;@pedalboard/components&lt;/code&gt; (which depends on it) gets selected too. That's exactly the behavior we want in CI.&lt;/p&gt;

&lt;p&gt;I also had to update &lt;code&gt;yarn bundle&lt;/code&gt; references in individual package scripts to &lt;code&gt;pnpm run bundle&lt;/code&gt;, and swap &lt;code&gt;--untraced yarn.lock&lt;/code&gt; to &lt;code&gt;--untraced pnpm-lock.yaml&lt;/code&gt; in the Chromatic commands.&lt;/p&gt;

&lt;p&gt;One subtlety: the coverage script passes extra flags to the test scripts recursively. With Yarn the syntax was:&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="nl"&gt;"coverage:all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yarn workspaces foreach -pRv run test -- --coverage --silent"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The natural pnpm equivalent would be:&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="nl"&gt;"coverage:all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r run test -- --coverage --silent"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But pnpm v9 changed how &lt;code&gt;--&lt;/code&gt; is handled - it now passes the &lt;code&gt;--&lt;/code&gt; separator through to the underlying script, so jest ends up receiving &lt;code&gt;"--" "--coverage" "--silent"&lt;/code&gt; and treats &lt;code&gt;--&lt;/code&gt; as a test name pattern instead of a separator. The fix: drop the extra &lt;code&gt;--&lt;/code&gt; and pass the flags directly:&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="nl"&gt;"coverage:all"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm -r run test --coverage --silent"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pnpm passes unrecognized flags through to the script, so &lt;code&gt;--coverage&lt;/code&gt; reaches jest correctly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating Lerna
&lt;/h2&gt;

&lt;p&gt;Lerna's config needed two changes. The &lt;code&gt;npmClient&lt;/code&gt; field changed from &lt;code&gt;"npm"&lt;/code&gt; to &lt;code&gt;"pnpm"&lt;/code&gt;, and I removed the now-obsolete &lt;code&gt;bootstrap&lt;/code&gt; command block:&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;"npmClient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&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"&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;"ignoreChanges"&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="s2"&gt;"ignored-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*.md"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore(release): publish %s"&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;"version"&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chore(release): version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"allowBranch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"master"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"conventionalCommits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;span class="nl"&gt;"packages"&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="s2"&gt;"packages/*"&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;"independent"&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;Another Yarn holdover was the &lt;code&gt;resolutions&lt;/code&gt; field in the root &lt;code&gt;package.json&lt;/code&gt;. pnpm uses its own format for dependency overrides:&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;"pnpm"&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;"overrides"&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;"parse-url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.1.0"&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;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;h2&gt;
  
  
  The pnpm Strictness Tax
&lt;/h2&gt;

&lt;p&gt;Here's where things got interesting. pnpm's strict dependency isolation is one of its biggest selling points - it prevents phantom dependencies (using packages you haven't explicitly declared). But it also means that things that "just worked" under Yarn's hoisting can break.&lt;/p&gt;

&lt;p&gt;I hit six cases of this:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Missing &lt;code&gt;@types/react&lt;/code&gt; in the hooks package
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error TS7016: Could not find a declaration file for module 'react'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My &lt;code&gt;@pedalboard/hooks&lt;/code&gt; package uses React (it's a hooks library, after all) but only declared &lt;code&gt;react&lt;/code&gt; as a &lt;code&gt;peerDependency&lt;/code&gt; - no &lt;code&gt;@types/react&lt;/code&gt; in &lt;code&gt;devDependencies&lt;/code&gt;. Under Yarn, the types were hoisted from &lt;code&gt;@pedalboard/components&lt;/code&gt; and available everywhere. Under pnpm, each package only sees its own declared dependencies.&lt;/p&gt;

&lt;p&gt;Fix: Added &lt;code&gt;@types/react&lt;/code&gt; to the hooks package's &lt;code&gt;devDependencies&lt;/code&gt;. Honestly, this should have been there all along - pnpm just caught the sloppy dependency declaration.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Shared esbuild config couldn't find its dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find module 'esbuild-sass-plugin'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;esbuild.config.js&lt;/code&gt; lives at the monorepo root and is called from package directories via &lt;code&gt;node ../../esbuild.config.js&lt;/code&gt;. It requires &lt;code&gt;esbuild&lt;/code&gt; and &lt;code&gt;esbuild-sass-plugin&lt;/code&gt;, but those were only declared in &lt;code&gt;packages/components&lt;/code&gt;. Node resolves &lt;code&gt;require()&lt;/code&gt; relative to the file's location (the root), not the CWD. Under Yarn, hoisting put them in the root &lt;code&gt;node_modules&lt;/code&gt;. Under pnpm, they weren't there.&lt;/p&gt;

&lt;p&gt;Fix: Added &lt;code&gt;esbuild&lt;/code&gt; and &lt;code&gt;esbuild-sass-plugin&lt;/code&gt; to the root &lt;code&gt;devDependencies&lt;/code&gt;. Since the config file lives at the root, its dependencies should be declared there.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The &lt;code&gt;postcss&lt;/code&gt; module in the stylelint plugin
&lt;/h3&gt;

&lt;p&gt;Same story - &lt;code&gt;@pedalboard/stylelint-plugin-craftsmanlint&lt;/code&gt; imports &lt;code&gt;postcss&lt;/code&gt; types but only had &lt;code&gt;stylelint&lt;/code&gt; (which depends on &lt;code&gt;postcss&lt;/code&gt;) as a direct dependency. pnpm doesn't allow accessing transitive dependencies.&lt;/p&gt;

&lt;p&gt;Fix: Added &lt;code&gt;postcss&lt;/code&gt; as an explicit &lt;code&gt;devDependency&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The &lt;code&gt;workspace:&lt;/code&gt; Protocol for Inter-Package Dependencies
&lt;/h3&gt;

&lt;p&gt;This was the most subtle one. The &lt;code&gt;@pedalboard/components&lt;/code&gt; package depends on &lt;code&gt;@pedalboard/hooks&lt;/code&gt; with a regular version range:&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;"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;"@pedalboard/hooks"&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.3.1"&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;Under Yarn, workspace symlinks meant Jest could transform and test this dependency just fine. Under pnpm, the module resolution goes through &lt;code&gt;.pnpm/&lt;/code&gt; store paths, which tripped up Jest's &lt;code&gt;transformIgnorePatterns&lt;/code&gt; - Jest refused to transform the ESM &lt;code&gt;export&lt;/code&gt; statements in the hooks package.&lt;/p&gt;

&lt;p&gt;The proper pnpm fix is the &lt;code&gt;workspace:&lt;/code&gt; protocol:&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;"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;"@pedalboard/hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"workspace:^"&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 tells pnpm to always link to the local workspace package. During &lt;code&gt;pnpm publish&lt;/code&gt;, it automatically replaces &lt;code&gt;workspace:^&lt;/code&gt; with the actual version range like &lt;code&gt;^0.3.1&lt;/code&gt;. Clean, explicit.&lt;/p&gt;

&lt;p&gt;I also updated every other inter-workspace dependency to use &lt;code&gt;workspace:^&lt;/code&gt;. For example, &lt;code&gt;@pedalboard/components&lt;/code&gt; also depends on &lt;code&gt;@pedalboard/stylelint-plugin-craftsmanlint&lt;/code&gt; as a devDependency, which got the same treatment.&lt;/p&gt;

&lt;p&gt;One thing to watch out for: any time you change a &lt;code&gt;workspace:^&lt;/code&gt; specifier, you need to run &lt;code&gt;pnpm install&lt;/code&gt; locally and commit the updated &lt;code&gt;pnpm-lock.yaml&lt;/code&gt;. CI runs with &lt;code&gt;--frozen-lockfile&lt;/code&gt; by default, so it will fail hard if the lockfile doesn't match &lt;code&gt;package.json&lt;/code&gt;. I learned this the fun way when the CI pipeline blew up with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERR_PNPM_OUTDATED_LOCKFILE Cannot install with "frozen-lockfile" because
pnpm-lock.yaml is not up to date with packages/components/package.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Just run &lt;code&gt;pnpm install&lt;/code&gt; and commit the lockfile. Simple fix, easy to forget.&lt;/p&gt;

&lt;p&gt;But that alone wasn't enough. Jest was still choking on the &lt;code&gt;@pedalboard/hooks&lt;/code&gt; ESM code. The root cause: pnpm's module resolution routes workspace packages through its &lt;code&gt;.pnpm/&lt;/code&gt; store paths, and Jest's default &lt;code&gt;transformIgnorePatterns&lt;/code&gt; excludes everything in &lt;code&gt;node_modules&lt;/code&gt; - including those store paths. The fix was a one-liner in &lt;code&gt;jest.config.base.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;transformIgnorePatterns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/(?!@pedalboard)&lt;/span&gt;&lt;span class="dl"&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 tells Jest: "ignore &lt;code&gt;node_modules&lt;/code&gt; for transformation, &lt;em&gt;except&lt;/em&gt; for anything under &lt;code&gt;@pedalboard&lt;/code&gt;." With that, Jest correctly transforms the workspace packages' TypeScript/ESM source. It's a small change with an outsized impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Storybook's &lt;code&gt;@storybook/node-logger&lt;/code&gt; version conflict
&lt;/h3&gt;

&lt;p&gt;This one only showed up when running Chromatic. The Storybook build failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Cannot find module 'storybook/internal/node-logger'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;storybook/internal/node-logger&lt;/code&gt; is a v8+ internal path that doesn't exist in Storybook v7. But the project uses v7. What happened: pnpm's resolver pulled in &lt;code&gt;@storybook/node-logger@8.6.14&lt;/code&gt; as a transitive dependency from somewhere, and its shim tries to import from the v8 internal path. Under Yarn, the v7 version was consistently hoisted everywhere.&lt;/p&gt;

&lt;p&gt;Fix: pin it in pnpm's &lt;code&gt;overrides&lt;/code&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;"pnpm"&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;"overrides"&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;"@storybook/node-logger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7.6.21"&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;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;h3&gt;
  
  
  6. Missing &lt;code&gt;style-loader&lt;/code&gt; and &lt;code&gt;css-loader&lt;/code&gt; in components
&lt;/h3&gt;

&lt;p&gt;After fixing the logger, Storybook's build still failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Module not found: Error: Can't resolve 'style-loader'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Storybook config in &lt;code&gt;packages/components/.storybook/main.js&lt;/code&gt; uses &lt;code&gt;@storybook/addon-styling-webpack&lt;/code&gt; with explicit webpack rules that reference &lt;code&gt;style-loader&lt;/code&gt; and &lt;code&gt;css-loader&lt;/code&gt;. Under Yarn, these were hoisted from some transitive dependency and available globally. Under pnpm, they need to be declared where they're actually used.&lt;/p&gt;

&lt;p&gt;Fix: added &lt;code&gt;style-loader&lt;/code&gt; and &lt;code&gt;css-loader&lt;/code&gt; to &lt;code&gt;@pedalboard/components&lt;/code&gt;'s &lt;code&gt;devDependencies&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD Updates
&lt;/h2&gt;

&lt;p&gt;The three GitHub Actions workflows needed the &lt;code&gt;pnpm/action-setup@v4&lt;/code&gt; step added before Node setup, and all &lt;code&gt;yarn&lt;/code&gt; commands replaced with &lt;code&gt;pnpm&lt;/code&gt; equivalents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;pnpm/action-setup@v4&lt;/code&gt; action reads the &lt;code&gt;packageManager&lt;/code&gt; field from &lt;code&gt;package.json&lt;/code&gt; to know which pnpm version to install. No extra configuration needed.&lt;/p&gt;

&lt;p&gt;I also updated a hardcoded &lt;code&gt;spawn('yarn', ...)&lt;/code&gt; call in the coverage aggregation script to use &lt;code&gt;pnpm&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: TypeScript Gaps Yarn Was Hiding
&lt;/h2&gt;

&lt;p&gt;This one isn't strictly a pnpm migration issue, but pnpm's strictness smoked it out. When building &lt;code&gt;@pedalboard/components&lt;/code&gt;, I got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error TS2550: Property 'fill' does not exist on type 'any[]'.
Do you need to change your target library? Try changing the 'lib' compiler option to 'es2015' or later.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Array.fill&lt;/code&gt; is ES2015. We were using it. TypeScript didn't know about it because &lt;code&gt;tsconfig.base.json&lt;/code&gt; had no &lt;code&gt;target&lt;/code&gt; or &lt;code&gt;lib&lt;/code&gt; set - which defaults to ES3. Yarn's hoisting had been accidentally providing a newer TypeScript version from one of the workspace packages, masking the gap.&lt;/p&gt;

&lt;p&gt;The fix: add explicit &lt;code&gt;target&lt;/code&gt; and &lt;code&gt;lib&lt;/code&gt; to the base config:&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;"compilerOptions"&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;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES2020"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"lib"&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="s2"&gt;"ES2020"&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="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;And since only the React packages (&lt;code&gt;components&lt;/code&gt;, &lt;code&gt;hooks&lt;/code&gt;, &lt;code&gt;media-loader&lt;/code&gt;) run in a browser, &lt;code&gt;DOM&lt;/code&gt; types belong there specifically - not in the base that's shared with Node.js tooling packages like the ESLint and Stylelint plugins:&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;"compilerOptions"&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;"lib"&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="s2"&gt;"ES2020"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DOM"&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;"ES2020"&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;A latent bug that was hiding in plain sight. pnpm didn't cause it - it just made it impossible to ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lerna Needed More Convincing
&lt;/h2&gt;

&lt;p&gt;After all that, I thought the migration was done. Then I tried to run &lt;code&gt;lerna publish&lt;/code&gt; and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lerna ERR! ENOWORKSPACES Usage of pnpm without workspaces is not supported.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The root cause: Lerna v5 has zero knowledge of the &lt;code&gt;workspace:^&lt;/code&gt; protocol. When it publishes &lt;code&gt;@pedalboard/components&lt;/code&gt;, it sends the &lt;code&gt;package.json&lt;/code&gt; as-is, &lt;code&gt;workspace:^&lt;/code&gt; and all. npm receives &lt;code&gt;"@pedalboard/hooks": "workspace:^"&lt;/code&gt; as a literal dependency version, tries to resolve it in the registry, finds nothing, and returns a deeply unhelpful 404.&lt;/p&gt;

&lt;p&gt;The real fix: upgrade Lerna. Support for the &lt;code&gt;workspace:&lt;/code&gt; protocol was added in v6.1.0, and in v8 (current latest) it's solid. Bump the version:&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;"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;"lerna"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.0.0"&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;In lerna v8, workspace discovery from &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; happens automatically - the old &lt;code&gt;useWorkspaces&lt;/code&gt; option was removed (it'll throw &lt;code&gt;ECONFIGWORKSPACES&lt;/code&gt; if you leave it in). Lerna v7+ also pulls in &lt;code&gt;nx&lt;/code&gt; as a task runner by default, which I don't need:&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;"npmClient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pnpm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"useNx"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;h3&gt;
  
  
  Registry auth with pnpm publish
&lt;/h3&gt;

&lt;p&gt;With lerna v5, &lt;code&gt;npmClient: pnpm&lt;/code&gt; only affected installs - lerna always called &lt;code&gt;npm publish&lt;/code&gt; internally regardless. The auth setup that &lt;code&gt;actions/setup-node&lt;/code&gt; generates worked fine because npm expands the &lt;code&gt;${NODE_AUTH_TOKEN}&lt;/code&gt; variable from its generated &lt;code&gt;.npmrc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Lerna v8 actually honors &lt;code&gt;npmClient&lt;/code&gt; for publishing too, so now &lt;code&gt;pnpm publish&lt;/code&gt; is doing the work. pnpm doesn't pick up &lt;code&gt;${NODE_AUTH_TOKEN}&lt;/code&gt; from the npm-generated config the same way. The fix: a &lt;code&gt;.npmrc&lt;/code&gt; at the project root that pnpm always reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org/&lt;/span&gt;
&lt;span class="err"&gt;//registry.npmjs.org/:&lt;/span&gt;&lt;span class="py"&gt;_authToken&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;${NODE_AUTH_TOKEN}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Safe to commit - no real token, just an env var reference that resolves correctly in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The migration is done and everything is green - all 7 packages build, all tests pass, and linting works (the few lint errors that remain are pre-existing, not migration-related).&lt;/p&gt;

&lt;p&gt;The biggest takeaway? pnpm's strict dependency isolation is a feature, not a bug. It caught several sloppy dependency declarations that Yarn's hoisting was silently papering over. Every package now explicitly declares what it uses, and that's just better engineering.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;workspace:&lt;/code&gt; protocol is a nice touch too - it makes inter-package dependencies explicit and handles the version replacement during publishing automatically. I wish I'd had this from the start.&lt;/p&gt;

&lt;p&gt;Was it worth the migration effort? yes, but there is still more to come which will explain why I chose to migrate to pnpm ;)&lt;/p&gt;




&lt;p&gt;&lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>monorepo</category>
      <category>npm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Testing After with AI Is Even Worse</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Sat, 28 Feb 2026 15:30:22 +0000</pubDate>
      <link>https://dev.to/mbarzeev/why-testing-after-with-ai-is-even-worse-4jc1</link>
      <guid>https://dev.to/mbarzeev/why-testing-after-with-ai-is-even-worse-4jc1</guid>
      <description>&lt;p&gt;Back then, I wrote a piece about “&lt;a href="https://dev.to/mbarzeev/why-testing-after-is-a-bad-practice-2pj5"&gt;Why Testing After Is a Bad Practice&lt;/a&gt;” where I laid out 4 main reasons why writing unit tests after the code is already implemented is a bad practice. Here’s a quick summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unconsciously complying with a given “reality”&lt;/strong&gt; - Writing tests after tends to shape the tests around the existing code, and not challenge it or make sure it does what it should&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emotional attachment to our code&lt;/strong&gt; - You’ve already developed some emotional attachment to the code, and you unconsciously avoid changing it, even when required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overly mocking&lt;/strong&gt; - Your code was poorly designed since nothing (mainly tests) required it to be modular and separated by concerns. This causes the tests written after to mock a lot of modules and services in order to test basic functionalities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gets neglected at the end&lt;/strong&gt; - When you write tests after, they tend to get neglected due to project delivery considerations. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These 4 main reasons still stand, but in the age of AI Agentic Coding, tests that were always (mistakenly) considered as tedious, boring tasks, became the ultimate candidate for code generation. &lt;br&gt;
I’m here to explain why writing tests with AI after the code was implemented is even worse than writing tests after without it. For that, I will take the 4 reasons mentioned above and show how they become even more critical. Let's start with the first one:&lt;/p&gt;

&lt;h2&gt;
  
  
  Unconsciously complying with a given “reality”
&lt;/h2&gt;

&lt;p&gt;AI Agentic coding, and in that sense, LLMs in general, have their reality. Since they are stateless, their reality is the accumulated "problem" context they pick along the way. When your code is already implemented, it becomes part of the context, and therefore, part of the reality for the AI Agent. In this case, the Agent will write tests that satisfy this reality, for example, if you implemented code that has a logical flaw within, it will write a test which asserts this logical flaw. &lt;br&gt;
In Andrej Karpathy’s talk &lt;a href="https://www.youtube.com/watch?v=7xTGNNLPyMI" rel="noopener noreferrer"&gt;Deep Dive into LLMs like ChatGPT&lt;/a&gt;, he claims that the better answer for a math problem (for example) will always be going through the mathematical flow and giving the answer at the end as opposed to giving the answer first, and then explaining the flow after, due to the same reason - when the agent gives the answer first, it becomes a part of its context and it will try to justify it as it continues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emotional attachment to our code
&lt;/h2&gt;

&lt;p&gt;This again refers to the “problem” context the agent has. It is not the developer who is emotionally attached to the code, but rather the Agent, which is attached to its context. If your prompt is “write unit tests for this module” it will write unit tests for this module, without examining if the module is currently doing what it should. So the “Emotional attachment to code” here is replaced by “Strict attachment to the context”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overly mocking
&lt;/h2&gt;

&lt;p&gt;I heard this from many developers - “The Agent has mocked every little thing, even the code it should test!”.&lt;br&gt;
Yes, this happens a lot, and for a “good” reason, if that can be said. The Agent was instructed to write tests, and the success criteria for the Agent is simple: make the tests pass, and it will do whatever it takes to achieve that goal. Extensive mocking is part of it. &lt;br&gt;
The reason for that usually comes from bad code design and coupling between modules, but sometimes it is just the Agent “getting carried away” and generating a full environment just to test a simple unit. In some cases it even mocks parts of the module under test, just to make the test pass. The horror.&lt;br&gt;
(BTW, I saw situations where in order to fix a test the agent simply decided to remove it - mission accomplished 😐)&lt;/p&gt;

&lt;h2&gt;
  
  
  Gets neglected at the end
&lt;/h2&gt;

&lt;p&gt;Maybe in the age of agentic coding it will still be addressed in the end, but it won’t be neglected (since the agent will write the tests for us), but here is exactly where our problem lies - the task of writing tests is being pushed to the end of the dev cycle, and under delivery pressure, we hand the steering wheel to the Agent and approve whatever it generates. The end result is bad tests which don’t supply us with the safety net we need (even more so, in the age of agentic coding).&lt;/p&gt;

&lt;h2&gt;
  
  
  The rise of TDD (and Planning)
&lt;/h2&gt;

&lt;p&gt;I see more and more community influencers speaking favorably on TDD when working with agentic coding, for example here is &lt;a class="mentioned-user" href="https://dev.to/mattpocockuk"&gt;@mattpocockuk&lt;/a&gt;   &lt;a href="https://www.youtube.com/watch?v=hYZdIwFIy-c" rel="noopener noreferrer"&gt;TDD Red Green Refactor is OP With Claude Code&lt;/a&gt;. &lt;br&gt;
TDD is nothing new and it was dismissed for years by developers who underestimated its long-term value. I’m super glad that agentic coding has brought it back to center stage and made its advantages clear. &lt;br&gt;
This, BTW, also goes for planning. Yes, planning! That part where you need to think before you start typing code. Agentic coding is showing us that TDD and proper planning are essential for better results. &lt;br&gt;
The red - green - refactor is helping the agent (and the engineer using it) to stay focused on the end result with small increments. Small increments always give better outcomes when it comes to agentic coding. &lt;/p&gt;

&lt;h2&gt;
  
  
  In conclusion
&lt;/h2&gt;

&lt;p&gt;Avoid delegating test writing to the end of the development cycle, especially to an Agent, since it will only try to satisfy the already written logic and will not provide you with the safety net you desperately need. If you can practice TDD as part of your work with agentic coding, that’s even better. You can incorporate TDD into the agentic coding skills, where the agent will write the test first, write the minimal code to make it pass, and then refactor. Combined with solid planning, this approach produces more predictable and more trustworthy results.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@pawel_czerwinski?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Pawel Czerwinski&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/red-and-blue-wallpaper-6lQDFGOB1iw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>Replacing a Plop React component generator with a Claude Code Skill</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Fri, 06 Feb 2026 13:14:34 +0000</pubDate>
      <link>https://dev.to/mbarzeev/replacing-a-plop-react-component-generator-with-a-claude-code-skill-5do</link>
      <guid>https://dev.to/mbarzeev/replacing-a-plop-react-component-generator-with-a-claude-code-skill-5do</guid>
      <description>&lt;h2&gt;
  
  
  The challenge in the age of Agentic coding
&lt;/h2&gt;

&lt;p&gt;One of the biggest challenges emerging in the age of agentic coding is determinism and standards-enforcement. In large organizations this becomes even more critical, where many developers are using agentic coding to produce a lot of code modifications, and maintaining standards and good quality becomes nearly impossible without the right tools.  &lt;/p&gt;

&lt;p&gt;This, however, is not a new issue. Large organizations always struggled with this and automatic tools were created to reduce the friction of standard and quality enforcement, such as &lt;a href="https://eslint.org/" rel="noopener noreferrer"&gt;ESlint&lt;/a&gt;, &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;, &lt;a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" rel="noopener noreferrer"&gt;Git commit hooks&lt;/a&gt;, tests, to name a few.&lt;br&gt;&lt;br&gt;
One of the tools which helps avoiding misalignment is a generator tool which helps developers scaffold and ramp-up components in a consistent manner, quickly and by given templates.&lt;br&gt;&lt;br&gt;
An example for such a tool is &lt;a href="https://plopjs.com/" rel="noopener noreferrer"&gt;Plop&lt;/a&gt;, which is a generator that can rely on templates and developer’s input to scaffold code. I actually &lt;a href="https://dev.to/mbarzeev/creating-a-react-component-generator-20l"&gt;wrote an article about creating such a tool&lt;/a&gt; for scaffolding a React component, along with tests, Storybook story etc.&lt;/p&gt;
&lt;h2&gt;
  
  
  But...
&lt;/h2&gt;

&lt;p&gt;Thinking about the agentic coding tools we’re all using, using Plop appeared to be somewhat redundant, and I wondered if I could convert my Plop generator for React components to a Claude Code &lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;Skill&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, let’s be on the same page on what Claude skill is -   &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Skills extend what Claude can do. …Claude adds it to its toolkit.  Claude Code skills follow the Agent Skills open standard, which works across multiple AI tools.  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From &lt;a href="https://code.claude.com/docs/en/skills" rel="noopener noreferrer"&gt;here&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;I also understand that Skill is the evolution of Slash Commands, while commands were a simple MD file, Skill is more robust, that can have its own context content, meta data and as the docs say, can be reused in different agents.&lt;/p&gt;

&lt;p&gt;For the demonstration here, I will be using VSCode with Claude installed&lt;br&gt;&lt;br&gt;
All the code can be found under my &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard monorepo&lt;/a&gt;.&lt;br&gt;&lt;br&gt;
Let’s begin - &lt;/p&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;I would like to have a Skill called &lt;code&gt;/create-react-component&lt;/code&gt;.&lt;br&gt;
When using this Skill, the developer will input the desired component’s name and once given, it will create the following files under the the &lt;code&gt;/src/&amp;lt;component-name&amp;gt;&lt;/code&gt; directory (in the “components” package):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Index.tsx - the component main file&lt;/li&gt;
&lt;li&gt;Index.css - the components CSS&lt;/li&gt;
&lt;li&gt;Index.stories.tsx - the component’s Storybook story&lt;/li&gt;
&lt;li&gt;Index.test.tsx - the component’s unit test&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These files will be made according to templates for complete result determinism.&lt;/p&gt;
&lt;h2&gt;
  
  
  Let’s start
&lt;/h2&gt;

&lt;p&gt;I’m going to do this experiment on components packages which reside on my &lt;a href="https://github.com/mbarzeev/pedalboard" rel="noopener noreferrer"&gt;Pedalboard project&lt;/a&gt;. This project was manually written until now, so this is the first time I’m introducing  agentic coding to it.&lt;br&gt;&lt;br&gt;
The first thing I would do is to initialize the project as a Claude Code project, using the &lt;code&gt;/init&lt;/code&gt; command -&lt;br&gt;&lt;br&gt;
Claude goes over the project, inspects it and creates a new &lt;a href="https://github.com/mbarzeev/pedalboard/blob/master/CLAUDE.md" rel="noopener noreferrer"&gt;CLAUDE.md&lt;/a&gt; file. In there it stores all that it found about the project, like that it’s a monorepo, and it runs with Yarn, and uses Jest for testing etc.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a custom Skill
&lt;/h2&gt;

&lt;p&gt;Skills in Claude reside under the &lt;code&gt;./claude/skills&lt;/code&gt; directory.&lt;br&gt;
We can prompt Claude to create a skill&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Create a Claude skill called create-react-component
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude went over the code in the entire monorepo and came up with this skill.md:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;create-react-component&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Create&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;React&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;component&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;packages/components&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;following&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;established&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conventions"&lt;/span&gt;
&lt;span class="na"&gt;argument-hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ComponentName&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;


Create a new React component in the &lt;span class="sb"&gt;`packages/components`&lt;/span&gt; package following the established conventions.&lt;span class="sb"&gt;


&lt;/span&gt;&lt;span class="gu"&gt;## Component: $ARGUMENTS&lt;/span&gt;&lt;span class="sb"&gt;


&lt;/span&gt;Create a folder &lt;span class="sb"&gt;`packages/components/src/$ARGUMENTS/`&lt;/span&gt; with the following files:&lt;span class="sb"&gt;


&lt;/span&gt;&lt;span class="gu"&gt;### 1. `index.tsx` — Component implementation&lt;/span&gt;
Use the template from &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;templates/component.tsx&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;templates/component.tsx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;:
&lt;span class="p"&gt;-&lt;/span&gt; Replace &lt;span class="sb"&gt;`{{ComponentName}}`&lt;/span&gt; with &lt;span class="sb"&gt;`$ARGUMENTS`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Replace &lt;span class="sb"&gt;`{{component-name}}`&lt;/span&gt; with the kebab-case version&lt;span class="sb"&gt;


&lt;/span&gt;&lt;span class="gu"&gt;### 2. `index.test.tsx` — Jest unit tests&lt;/span&gt;
Use the template from &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;templates/component.test.tsx&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;templates/component.test.tsx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;:
&lt;span class="p"&gt;-&lt;/span&gt; Replace &lt;span class="sb"&gt;`{{ComponentName}}`&lt;/span&gt; with &lt;span class="sb"&gt;`$ARGUMENTS`


&lt;/span&gt;&lt;span class="gu"&gt;### 3. `index.stories.tsx` — Storybook story&lt;/span&gt;
Use the template from &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;templates/component.stories.tsx&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;templates/component.stories.tsx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;:
&lt;span class="p"&gt;-&lt;/span&gt; Replace &lt;span class="sb"&gt;`{{ComponentName}}`&lt;/span&gt; with &lt;span class="sb"&gt;`$ARGUMENTS`


&lt;/span&gt;&lt;span class="gu"&gt;### 4. `index.scss` — Component styles&lt;/span&gt;
Use the template from &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;templates/component.scss&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;templates/component.scss&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;:
&lt;span class="p"&gt;-&lt;/span&gt; Replace &lt;span class="sb"&gt;`{{component-name}}`&lt;/span&gt; with the kebab-case version of &lt;span class="sb"&gt;`$ARGUMENTS`


&lt;/span&gt;&lt;span class="gu"&gt;### 5. Update barrel export&lt;/span&gt;
Add a named export to &lt;span class="sb"&gt;`packages/components/index.ts`&lt;/span&gt;:

export {default as $ARGUMENTS} from './src/$ARGUMENTS';

After creating all files, run &lt;span class="sb"&gt;`yarn test`&lt;/span&gt; and &lt;span class="sb"&gt;`yarn lint`&lt;/span&gt; from &lt;span class="sb"&gt;`packages/components/`&lt;/span&gt; to verify everything works.

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might have noticed that the Skill relies on some templates. Claude “saw” that the project used Plop with handlebars templates, it took the templates and created a newer version of them under the &lt;code&gt;.claude/skills/create-react-component/templates&lt;/code&gt; path. So what we currently have is this file structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.claude/
├── settings.local.json
└── skills/
    └── create-react-component/
        ├── SKILL.md                        &lt;span class="c"&gt;# Main instructions with frontmatter&lt;/span&gt;
        └── templates/
            ├── component.tsx               &lt;span class="c"&gt;# Component template&lt;/span&gt;
            ├── component.test.tsx          &lt;span class="c"&gt;# Test template&lt;/span&gt;
            ├── component.stories.tsx       &lt;span class="c"&gt;# Story template&lt;/span&gt;
            └── component.scss              &lt;span class="c"&gt;# Style template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It appears that unlike Plop, the templates used by the Skill do not have to be Handlebars templates, the Skill can settle with double curly brackets (“{{...}}”) for args placeholders.&lt;br&gt;
So we have the Skill instructions and we have the templates ready - it’s time to try it out&lt;/p&gt;
&lt;h2&gt;
  
  
  Trying it out…
&lt;/h2&gt;

&lt;p&gt;For Claude to register the new Skill we need to restart it, and after we do, we prompt the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/create-react-component TestComponent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hmmm… Claude nicely created 4 files, let’s look at the index file for the component to see what we got:&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="cm"&gt;/**
* Copyright (c) 2025-present, Matti Bar-Zeev.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/&lt;/span&gt;


&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;react&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="nx"&gt;PropTypes&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;prop-types&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./index.scss&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TestComponentProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TestComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TestComponentProps&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;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-component&lt;/span&gt;&lt;span class="dl"&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;span&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;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;       &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;TestComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;propTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;


&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TestComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not bad, but as you can see, Claude took my “legacy” Plop templates, which were meant for JS (and not TypeScript) and simply migrated them, as is, to the Skill’s template. Let’s change them to fit TS, and then use the Skill again to make sure Claude indeed always follows them. Here is my prompt for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Take the templates from .claude/skills/create-react-component/templates and convert them to be TypeScript complaint, removing redundant code, like propTypes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after Claude did some required modifications, let’s run the Skill to create the component again&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="cm"&gt;/**
* Copyright (c) 2025-present, Matti Bar-Zeev.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/&lt;/span&gt;


&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;react&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./index.scss&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;TestComponentProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
   &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TestComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;TestComponentProps&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;return &lt;/span&gt;&lt;span class="p"&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="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test-component&lt;/span&gt;&lt;span class="dl"&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;span&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;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/span&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;       &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;TestComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better :) &lt;/p&gt;

&lt;p&gt;Tests are passing and also Storybook story looks as it should:&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%2Fep5l92ticw2o2s5rky92.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%2Fep5l92ticw2o2s5rky92.png" alt=" " width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Is using template files a good thing?
&lt;/h2&gt;

&lt;p&gt;One of the questions I’ve asked myself is why not having the SKILL.md also contain the templates and be done with it?&lt;br&gt;
So having templates as a separated content offers a few advantages:&lt;br&gt;
Smaller initial context - The SKILL.md file stays concise with just instructions. The templates are only loaded when the skill actually runs (via Read tool calls).&lt;br&gt;
Cleaner separation - Instructions vs. template content are clearly separated, making the skill easier to maintain.&lt;br&gt;
On-demand loading - Templates are fetched only when needed, rather than always being present in the skill definition.&lt;br&gt;
Templates reuse - other Skills can use templates from another Skill.&lt;/p&gt;

&lt;p&gt;That being said, it’s important to note that reading the templates requires a read tool call, which might slow things down a bit, but I think that the overall advantages mentioned above are worth it.&lt;/p&gt;

&lt;p&gt;As mentioned about, You can find the new skill markdown and templates in the &lt;a href="https://github.com/mbarzeev/pedalboard/blob/master/.claude/skills/create-react-component/SKILL.md" rel="noopener noreferrer"&gt;Pedalboard monorepo on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Cheers&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@artchicago?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Art Institute of Chicago&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-painting-of-a-field-of-flowers-and-trees-dynKVvbZ-5Q?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>agents</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Responsible Vibe Coding</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Sat, 11 Oct 2025 15:05:55 +0000</pubDate>
      <link>https://dev.to/mbarzeev/responsible-vibe-coding-6g3</link>
      <guid>https://dev.to/mbarzeev/responsible-vibe-coding-6g3</guid>
      <description>&lt;p&gt;With the rise of AI agentic coding, &lt;a href="https://en.wikipedia.org/wiki/Vibe_coding" rel="noopener noreferrer"&gt;Vibe-Coding&lt;/a&gt; entered our dev life and stirred quite a storm. The web is full of vibe-coding opinions and rants, some say it's a new dawn, some lash at it - but this post is not any of these.&lt;/p&gt;

&lt;p&gt;In this post, I would like to share some of my current insights on the process, especially what works for me and how to avoid the many "traps" vibe coding may lead you into.&lt;br&gt;
As I see it, like in many technologies and tools, it is never the tech's fault but rather the wrong usage of it. But unlike other techs, vibe coding kinda lures you into using it wrongly. It requires very strict self-discipline.&lt;/p&gt;

&lt;p&gt;If we have to, we can separate vibe coders into two - those who know and understand the technology the agent uses and have a good architectural mindset, vs. those who just wanna get something working fast without caring what's going on under the hood. We can go further and separate it even more - those who know how to articulate their intentions well in plain English and those who don't.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rise of the "verbal programmer"
&lt;/h2&gt;

&lt;p&gt;Tech has a repeating growth cycle - from something that is unnatural for humans, like typing code, into something that blends smoothly with our natural mental model, like touch screens, voice AI assistants, VR, etc.&lt;br&gt;
The software development world was always dominated by those who could think like a computer. Take an idea they have and translate it into "if" statements and loops. This is not the case anymore.&lt;/p&gt;

&lt;p&gt;AI coding requires a different set of skills, one might call "soft skills." It is how you communicate your intentions in a verbal way. The top skill required is for one to master the English language (LLMs are mainly trained in the English language, that's a cold fact), and if you wish to receive good outcomes from it, you must master this communication skill.&lt;br&gt;
It is no longer how fast you type or how well you know a syntax, but how accurately you can express your intentions and requirements.&lt;/p&gt;

&lt;p&gt;The "verbal programmer" is the one who will get the better results and faster than the "analytic programmer."&lt;br&gt;
I simply love it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Not just "What" but also "How"
&lt;/h2&gt;

&lt;p&gt;If someone asked me what is the most crucial thing to understand about coding with AI, I would say, "Understand that it is a &lt;strong&gt;development assistant&lt;/strong&gt;, not a code magician."&lt;/p&gt;

&lt;p&gt;Many claim that they're frustrated with the results they get. I used to say that too when I first started playing with it, and I think I know why it happened - we used to think that we only need to tell our agent what to do, but for better results, you also need to tell it how you want it to be done. And for the "how," you need to know your shit. If you leave it to the agent, you're in for big surprises.&lt;/p&gt;

&lt;p&gt;So instead of "get the list of songs from this API and render it as a list," you might wanna say, "get the list of songs using the fetch API and render it in a list by reusing the Song component as the rendering component for a single song, having the song unique ID as the key."&lt;/p&gt;

&lt;p&gt;It is more than ok to ask the agent to give you options you can choose from and create a small pros and cons comparison. In the end, you are the one who needs to decide what direction to go.&lt;br&gt;
If you are not familiar with the solutions offered, it is a great opportunity to learn about them, the first step being to ask the agent why it thinks this is the best solution for the problem at stake.&lt;/p&gt;
&lt;h2&gt;
  
  
  Which model?
&lt;/h2&gt;

&lt;p&gt;I must admit that I don't have an answer for that, and TBH I don't think that there will ever be a good answer for that. The model race is happening as we speak, and almost every day you hear of another model version which is supposed to be better at this and that. The FOMO is overwhelming and distracting us from the main objective.&lt;/p&gt;

&lt;p&gt;To quote Westley,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;del&gt;Life&lt;/del&gt; software development is pain, Highness, anyone who says differently is selling something." &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Code still remains hard, and creating a complex application is still a challenge.&lt;br&gt;
Like you, I used a bunch of them, jumping from one to another, and I managed to get good results from what was considered a "poor at coding" model, and bad results from those who claimed to be good at coding.&lt;/p&gt;

&lt;p&gt;It's true that model training stops at different times and the weight parameters can vary to address coding better. Still, my 2 cents on the matter - the model you choose is not that critical. Most of us don't deal with cutting-edge tech in our day-to-day, and for the large majority of code generation, the top models are pretty much the same.&lt;/p&gt;

&lt;p&gt;I tend to think of it as a guitar player messing around with many overdrive pedals when what he should really be focusing on is playing the riff correctly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architectural concerns
&lt;/h2&gt;

&lt;p&gt;Many have said it before - agents don't excel at making good architectural decisions.&lt;br&gt;
They are absolutely right (pun intended).&lt;/p&gt;

&lt;p&gt;If we take React, for example, unless you specify it, there is a good chance the agents will attempt to write everything in a single component with no real thinking about reuse or SoC. I don't blame them. How could they know that you planned to use the SongList component in another component? How should they know that the SongList component should not be aware of the logic which fetches the data it renders?&lt;/p&gt;

&lt;p&gt;Another example is creating Express server endpoints. If you don't specify it, there is nothing holding the agent from creating the routes, the controllers, and the services which handle the request all in the same file.&lt;/p&gt;

&lt;p&gt;While determining the next token in creating a component might be "straightforward" for the model, the architectural decisions are more complex, and the content on which the model has trained probably does not represent your organization's business logic. Chances are it will fail to make the right decisions then.&lt;br&gt;
Again, this is not saying the model isn't "smart" (like I heard many say). It simply means that it hasn't got the context it needs to make the right decisions. And the context needed for these kinds of decisions is massive.&lt;/p&gt;

&lt;p&gt;Yet, having written these lines, I started wondering... what if the agent writes the routes, controller, and services in the same file? Should we fear spaghetti code in the era of AI?&lt;br&gt;
After all, I can give the spaghetti code to my agent and it will know how to untangle it... right?&lt;br&gt;
I have my answer for that (spoiler alert - "don't encourage spaghetti code!"), but I also leave it here for you to ponder about as well. Leave your answers in the comments below.&lt;/p&gt;

&lt;p&gt;As mentioned before, our agent also doesn't always know how to choose the most suitable solutions for the problem. For example, if you tell it to create code for fetching data from a certain endpoint, you will most likely end up using the "axios" lib. Why? Because this is the pattern the model has seen commonly in many projects, but it does not mean that it suits your project. You would be quite satisfied with the native fetch API. I, for one, would always prefer the native APIs first over installing third-party libraries. In fact, I have this as a stored "memory" in Cursor:&lt;/p&gt;

&lt;p&gt;"The user prefers that the assistant always use native APIs and validate official documentation before implementing custom in-house or third-party solutions."&lt;/p&gt;
&lt;h2&gt;
  
  
  What works for me
&lt;/h2&gt;

&lt;p&gt;I cannot consider what I'm doing "vibe-coding". For lack of a better term, I will call it "Responsible vibe-coding"&lt;br&gt;
Here how it goes - &lt;/p&gt;
&lt;h3&gt;
  
  
  Todos
&lt;/h3&gt;

&lt;p&gt;I first start with a clear and very precise TODO. This can be a simple feature or a bug fix. I believe that everyone has their own list of what should be done, whether it is an MD file with bullets or a Jira board.&lt;br&gt;
This allows me to get a high-level view of what should be done and consider if there are tasks that should come before or after one another. The smaller the task is, the better. Once I have this list sorted out (and it's a dynamic list, so tasks can be added, removed, or reordered in it), I can continue to the next steps.&lt;/p&gt;

&lt;p&gt;I found that categorizing the list helps me stay focused on what's important. Here are my categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MVP&lt;/strong&gt; - The things that the product cannot be completed without. Every task there is mandatory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs&lt;/strong&gt; - The list of bugs that I found along the way or that I know of. These tasks completion is also mandatory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring&lt;/strong&gt; - Anything I would like to re-organize or write in a different way, while not altering the current functionality. Especially when you're vibe-coding, soon enough you will discover areas where things could have been done differently to improve code reusing, ease future maintenance etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nice to have&lt;/strong&gt; - Well, this goes without saying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some of the tasks required a plan. A PRD (&lt;strong&gt;P&lt;/strong&gt;roduct &lt;strong&gt;R&lt;/strong&gt;equirements &lt;strong&gt;D&lt;/strong&gt;ocument) is in order, and this is the first stage I start to request the AI agent for assistance. &lt;/p&gt;
&lt;h3&gt;
  
  
  The plan - PRD (or SPEC)
&lt;/h3&gt;

&lt;p&gt;I would take the task at hand and ask the agent to create a PRD for it. An example for such a prompt may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For the task of &amp;lt;task from the todo here&amp;gt;, create a PRD which consists of the following: a brief of the current status, a solution overview, very detailed implementation phases, each containing steps that can be marked once completed. Each phase should be a deliverable phase, meaning the application is still shippable once the phase is completed. There is no need for risk assessments, future planning, or testing recommendations.
Write the PRD to a prd-&amp;lt;task-name&amp;gt;.md under the prd directory.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recently, &lt;a href="https://cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; released a &lt;a href="https://cursor.com/docs/agent/modes#plan" rel="noopener noreferrer"&gt;"Plan" mode&lt;/a&gt; to their chat, which does a pretty good job, asking leading and clarification questions and producing a good plan for the agent to follow.&lt;/p&gt;

&lt;p&gt;When the agent completes this task, you should have a PRD markdown file under the prd folder of your project. I think it makes a lot of sense to have a dedicated directory for those PRDs in your project. They come in very handy when you want to communicate with others and hand in details of the implementation.&lt;/p&gt;

&lt;p&gt;The most critical part in this step is to review the PRD.&lt;/p&gt;

&lt;p&gt;Given that your task was small and well defined, the PRD should not be a long document. Go over the solution overview and see if it fits your requirements. Check the implementation phases and make sure that the agent did not insert any undesired steps, because it will try.&lt;br&gt;
Ask the agent a question about the decisions it took. Don't let it get away with the "You're absolutely right!" BS. Ask it to give the pros and cons of a decision you're not sure about and decide accordingly.&lt;/p&gt;
&lt;h3&gt;
  
  
  Implementing the PRD
&lt;/h3&gt;

&lt;p&gt;When you're satisfied with the PRD, it is time to move forward to the implementation. I usually go with,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start implementing phase 1.1 &amp;lt;the title for the phase&amp;gt;. Implement this phase only and don't continue until I approve your implementation.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agents do have the tendency to get carried away (I literally had an agent telling me, "Sorry, I got carried away," when I asked why it continued implementing the next phase).&lt;/p&gt;

&lt;p&gt;Once the agent finishes implementing the phase, go over the modified code, review it, see that you understand what it did, and if not, require explanations (this is, BTW, a great way to learn stuff as you go).&lt;br&gt;
Don't be quick to click that "Keep all" button. Take the time to review the changes; otherwise, you will lose track very quickly. If you don't understand what your agent is doing, you're in for some serious time loss.&lt;/p&gt;

&lt;p&gt;Also, if you think the agent modified too many files - more than was needed given your understanding of the architecture - you might be facing a vibe-code-smell (see section on that below), and you might wanna stop and see what you can do to better architect your code. The conclusions may lead you into adding some tasks under the "Refactoring" todo section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging
&lt;/h3&gt;

&lt;p&gt;Agents tend to insert a lot of console logs and other debugging tools when they generate the code. I don't mind it that much, and I think that when the feature is being implemented, it greatly helps to figure out if it works as expected.&lt;br&gt;
Loggers also help the agent find bugs in the implementation. I found myself, in several cases, giving the agent the log stack from my browser or Node server for it to figure out what went wrong and at what point, and it was very helpful.&lt;br&gt;
Having said that, it would be wise to clean up the massive logging after the implementation is completed, leaving only the crucial log messages. &lt;br&gt;
Clean up&lt;br&gt;
Surprisingly enough, the agent seems to avoid discarding unused files.&lt;br&gt;
Many times, I found the agent leaving "dead code" behind and moving on, and I ended up telling it, "Remove any redundant or dead code from the modified files." You'd be surprised at how much junk is accumulated during vibe-coding. &lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe code smell
&lt;/h2&gt;

&lt;p&gt;I came to the realization that there is such a thing as Vibe-code-smell, and it's important to identify it as soon as possible and act to un-smell it.&lt;br&gt;
Like in &lt;a href="https://en.wikipedia.org/wiki/Code_smell" rel="noopener noreferrer"&gt;traditional code-smell&lt;/a&gt;, this is an indication of "... weaknesses in design that may slow down development or increase the risk of bugs or failures in the future."&lt;br&gt;
Identifying the code-smell when vibe-coding is a bit more challenging, because you're less intimate with the code being written. But here are a few indications you can keep an eye on -&lt;/p&gt;

&lt;h3&gt;
  
  
  Too many modified files
&lt;/h3&gt;

&lt;p&gt;As simple as it sounds, this is when your agent performs modifications in too many files, way more than what you thought your prompt required. This usually hints that your code design is broken, perhaps SoC is not well implemented correctly. In React, for example, it may indicate props drilling or bad state management, perhaps a component that needs to be extracted into smaller components and reused properly.&lt;br&gt;
In any case, this is the first indication that your agent is running wild and that you are losing control over what it's doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  "You're absolutely right!"
&lt;/h3&gt;

&lt;p&gt;Careful now with how you accept this warm compliment from your agent. The model behind the agent, as you know, is a simple token-resolver machine in the end, and if you suggest an idea to it, it becomes part of its context, therefore, it will attempt to justify it in the next resolved tokens. Although you are very smart, you cannot be absolutely right all the time.&lt;br&gt;
Ask the agent to debate, show the pros and cons of your suggestion, and maybe offer other options with comparisons.&lt;/p&gt;

&lt;h3&gt;
  
  
  Too many iterations
&lt;/h3&gt;

&lt;p&gt;Sometimes you will see the agent going into a loop of trying to solve an issue, saying that it found the solution, implementing it, checking it, realizing it was not solved, and then repeating.&lt;br&gt;
For me, after the 3rd attempt, I will stop it and look at the code myself, while having the agent as an assistant in debugging it. Read what the agent is outputting in the chat to make sure it did not get carried away outside the scope of the issue at hand. This is also a good place to say that I'm not in favor of giving the agent free permission to run any command or tool at will. I always want to approve what it is about to execute when it's not just modifying code. Aside from avoiding the risks in it, this also gives me good control over stopping it from spiraling into endless iterations.&lt;/p&gt;

&lt;h2&gt;
  
  
  In conclusion
&lt;/h2&gt;

&lt;p&gt;Responsible Vibe Coding is not about resisting the new way of developing - it's about embracing it with awareness, discipline, and ownership. The power of AI-driven coding agents is undeniable, but that power comes with a responsibility: to stay in control, to think architecturally, and to communicate intentionally. The line between "let the agent handle it" and "guide the agent effectively" is thin, and learning to walk it makes all the difference between chaos and craftsmanship.&lt;/p&gt;

&lt;p&gt;Vibe coding can make us faster, but only if we remain thoughtful. It can make development smoother, but only if we don't surrender our understanding of what's happening under the hood. The agent can generate the code, but it's still our job to ensure it makes sense - structurally, logically, and contextually.&lt;/p&gt;

&lt;p&gt;In the end, Responsible Vibe Coding is not just a method - it's a mindset. It's about shifting from "prompt and pray" to "plan, guide, and verify." It's about treating the AI as a capable collaborator, not an infallible magician. When done right, it brings out the best in both human reasoning and machine efficiency. When done carelessly, it amplifies the very weaknesses we once hoped it would fix.&lt;/p&gt;

&lt;p&gt;So keep your architecture clean, your prompts precise, and your curiosity alive. Code responsibly, vibe intentionally.&lt;/p&gt;

&lt;p&gt;Cheers&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@luk10?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Lukas Tennie&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-close-up-of-a-watch-face-with-the-gears-missing-3dyDozzCORw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>ai</category>
      <category>architecture</category>
      <category>react</category>
    </item>
    <item>
      <title>Elegant Hybrid TS project’s build</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Tue, 25 Mar 2025 14:30:00 +0000</pubDate>
      <link>https://dev.to/mbarzeev/elegant-hybrid-ts-projects-build-5e04</link>
      <guid>https://dev.to/mbarzeev/elegant-hybrid-ts-projects-build-5e04</guid>
      <description>&lt;p&gt;Oh BTW, This post was 100% human written with no AI assistance whatsoever. All typos and bad grammar are my own. Enjoy :)&lt;/p&gt;




&lt;p&gt;I doubt you remember, but back at the time I wrote a piece about &lt;a href="https://dev.to/mbarzeev/hybrid-npm-package-through-typescript-compiler-tsc-150c"&gt;Hybrid NPM package through TypeScript Compiler (TSC)&lt;/a&gt;, which explained how you can leverage TSC to produce 2 artifact types (ESM and CJS) from a single source code.&lt;br&gt;
I thought it was pretty clever. Hell, I still do 😉&lt;/p&gt;

&lt;p&gt;Recently though, I came across one of &lt;a href="https://dev.to/mattpocockuk"&gt;Matt Pocock&lt;/a&gt;’s TS course lessons (which I highly recommend on btw) that suggested a more elegant way to achieve that. In this post I will migrate to that way and explain what I do so your AI agent won’t get left behind ;)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: It would be best if you go over the &lt;a href="https://dev.to/mbarzeev/hybrid-npm-package-through-typescript-compiler-tsc-150c"&gt;Hybrid NPM package through TypeScript Compiler (TSC)&lt;/a&gt; article, so you will get the context of what I’m about to write on.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p&gt;I have this package in my Pedalboard monorepo, called &lt;a href="https://github.com/mbarzeev/pedalboard/tree/master/packages/media-loader" rel="noopener noreferrer"&gt;@pedalboard/media-loader&lt;/a&gt; (that you should &lt;strong&gt;totally&lt;/strong&gt; check out if you’re a React dev struggling with media loading performance), which has a build process that generates 2 types of artifacts - ESM and CJS. In order to support that I’ve created 2 different &lt;code&gt;tsconfig.json&lt;/code&gt; files and in the build script I’m running them both.&lt;br&gt;
It looks something 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="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;"tsc --project tsconfig.esm.json &amp;amp; tsc --project tsconfig.cjs.json"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although there is nothing wrong with it, it kinda requires this plumbing in the &lt;code&gt;package.json&lt;/code&gt; file and all-in-all feels a bit of a brute-force-primitive. Fortunately there is a more elegant way to achieve that using the &lt;strong&gt;references&lt;/strong&gt; config. &lt;br&gt;
Here is how:&lt;/p&gt;

&lt;p&gt;Both the &lt;code&gt;tsconfig.esm.json&lt;/code&gt; and &lt;code&gt;tsconfig.cjs.json&lt;/code&gt; are in the root dir of the package, and to them I will add the “main” config file - &lt;code&gt;tsconfig.json&lt;/code&gt;, and its content looks 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;"references"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.esm.json"&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;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./tsconfig.cjs.json"&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;span class="nl"&gt;"files"&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="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;Let’s go over it real quick - it has a &lt;code&gt;references&lt;/code&gt; array which has 2 paths defined in it, one for each config file, and it has an empty array for the &lt;code&gt;files&lt;/code&gt; config, to ensure that the &lt;code&gt;tsconfig.json&lt;/code&gt; does not enforce any type checking, but rather leaves that to the referenced projects.&lt;/p&gt;

&lt;p&gt;Now we need to change the build script. Instead of what we have, we now write 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="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;"tsc -b"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;-b&lt;/code&gt; param, which tells TSC to run it in build mode, thus taking into consideration the different references in the main &lt;code&gt;tsconfig.json&lt;/code&gt; file. In other words, without the &lt;code&gt;-b&lt;/code&gt; param, it won’t work as we expect it to.&lt;/p&gt;

&lt;p&gt;And that is it!&lt;/p&gt;

&lt;p&gt;When we run our build script now, TSC will run both configurations and create both ESM and CJS artifacts. &lt;br&gt;
There is a lot more you can do with the project references like cache type check results (for faster builds) or define dependencies between projects, but in this case what we have is enough.&lt;/p&gt;

&lt;p&gt;The code is available on Github &lt;a href="https://github.com/mbarzeev/pedalboard/tree/master/packages/media-loader" rel="noopener noreferrer"&gt;@pedalboard/media-loader&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Be seeing you&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@trnavskauni?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Trnava University&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/person-writing-on-white-paper-7t1WvWuDDGk?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;      &lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>programming</category>
    </item>
    <item>
      <title>I've built the TodoMVC app with HTMX and lived to tell the story</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Tue, 12 Nov 2024 15:00:00 +0000</pubDate>
      <link>https://dev.to/mbarzeev/ive-built-the-todomvc-app-in-htmx-and-lived-to-tell-the-story-480p</link>
      <guid>https://dev.to/mbarzeev/ive-built-the-todomvc-app-in-htmx-and-lived-to-tell-the-story-480p</guid>
      <description>&lt;p&gt;In this post, I’ll walk you through my experiences building the &lt;a href="https://todomvc.com/" rel="noopener noreferrer"&gt;TodoMVC&lt;/a&gt; app using &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;HTMX&lt;/a&gt;. I'll cover the architectural considerations, handy tips, pros and cons, insights, and everything in between.&lt;/p&gt;

&lt;p&gt;The code can be found here, along with an elaborated README -  &lt;a href="https://github.com/mbarzeev/todomvc/tree/htmx/examples/htmx" rel="noopener noreferrer"&gt;https://github.com/mbarzeev/todomvc/tree/htmx/examples/htmx&lt;/a&gt; &lt;/p&gt;




&lt;h2&gt;
  
  
  The TodoMVC project
&lt;/h2&gt;

&lt;p&gt;Back when JavaScript frameworks were flooding the web ecosystem and a new, promising framework seemed to emerge every month as the next big thing, Addy Osmani set out to create a benchmark application. His goal was to push each framework to its reasonable limits, giving developers a sort of "speed-dating" experience to evaluate them quickly.&lt;/p&gt;

&lt;p&gt;This project was known as &lt;a href="https://todomvc.com/" rel="noopener noreferrer"&gt;TodoMVC&lt;/a&gt;, and you can still find its content relevant for technologies like React, Vue, Svelte etc. It has become the goto place for checking whether a technology fits your purposes or not. &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%2F8ryj0zpi03nle48yz2jc.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%2F8ryj0zpi03nle48yz2jc.png" alt="Image description" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  HTMX
&lt;/h2&gt;

&lt;p&gt;If you’re in tune with web dev trends you’ve probably heard or read about HTMX. This is a small JS project, aiming to simplify the way we build web apps, relying more on server rendered markup and AJAX. &lt;/p&gt;

&lt;p&gt;My interest in this technology stems from my "Back to Square One" approach, which is all about stepping off the frameworks and meta-frameworks roller coaster for a moment to take a fresh look at how we build things today—and explore ways to improve the experience for both us and our customers.&lt;/p&gt;

&lt;p&gt;HTMX fits this approach pretty well, relying on much less JS code for heavy client magic, and more on web standards and the http protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opportunity knocks
&lt;/h2&gt;

&lt;p&gt;Obviously the first place I went to check whether HTMX is something that one can rely on was the TodoMVC project, alas, there was no example written with this technology.&lt;/p&gt;

&lt;p&gt;Hear that Opportunity knocking?&lt;br&gt;
I can create this example and deepen my HTMX knowledge on the process, and hey - I might be able to contribute to the TodoMVC open-source project. This is a win-win-win situation. I like those.&lt;/p&gt;
&lt;h2&gt;
  
  
  First steps
&lt;/h2&gt;

&lt;p&gt;Gladly the TodoMVC project has a great contribution docs and it also provides a starting template, along with a detailed application spec. I took the template and spec and started inspecting them to get a better understanding on how the app client is constructed (the markup) and what functionality needs to be supported. &lt;/p&gt;

&lt;p&gt;This initial phase, as you will see later, will have serious implications on how the final HTMX app architecture will end up like. &lt;/p&gt;
&lt;h2&gt;
  
  
  HTMX app architecture
&lt;/h2&gt;

&lt;p&gt;I believe the key takeaway from this article is that you can't approach HTMX app architecture the same way you would with reactive, client-heavy JavaScript frameworks. Doing so would mean missing out on HTMX’s unique advantages and could lead to frustration, making it feel difficult or unintuitive. Building with HTMX is a completely different discipline. &lt;/p&gt;

&lt;p&gt;In HTMX most of the work is done on the server side, thus most of your architecture decisions are relevant for that tier, yet, you need to construct the client markup in a sensible way that will allow you to enjoy the different swapping strategies HTMX offers.&lt;/p&gt;

&lt;p&gt;In the case of TodoMVC, I was, for better or worse, constrained by the template’s markup. However, I didn’t have much desire to change the template anyway, as I wanted to see how HTMX would perform within these limitations.&lt;/p&gt;
&lt;h3&gt;
  
  
  Server architecture
&lt;/h3&gt;

&lt;p&gt;I will be using the &lt;a href="https://c4model.com/" rel="noopener noreferrer"&gt;C4 model&lt;/a&gt; in order to visualize the “server container” architecture. &lt;br&gt;
The server container has 4 components in it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TodosRouter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines the routes the client uses to interact with the app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TodosAPI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The "Model" - Holds and manipulates the todos data, handling all CRUD ops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Templating engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The "View" - Renders HTML from the data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TodosController&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The "Controller" - Handles requests, using TodosAPI to compose HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yes, you could do the entire thing in a single file, but since the application here is very conservative it allowed me to think deeper on how I would see an HTMX driven application being code-designed. &lt;/p&gt;

&lt;p&gt;One of the aspects of HTMX that really appealed to me is that the client directly receives content it can immediately parse and display, rather than receiving a JSON data structure that then has to be converted into markup.&lt;/p&gt;

&lt;p&gt;This means there must be a component responsible for "translating" the data into markup. That tier would be the templating engine. This approach allows us, perhaps in the future, to change how we “translate” data into markup without altering other parts of the system. &lt;/p&gt;

&lt;p&gt;The component which orchestrates it all is the controller, and it works with the API to get and manipulate the data, and then takes the result and passes it through the templating engine component in order to send markup to the client. &lt;/p&gt;

&lt;p&gt;The diagram below describes it better:&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%2F7dhxi7geo0s9yxj9bu79.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%2F7dhxi7geo0s9yxj9bu79.png" alt="Image description" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The tech stack
&lt;/h3&gt;

&lt;p&gt;It’s a good point to present the technologies we’re going to use for this application. Obviously these are my choices, and you can choose whatever alternative you feel comfortable with -&lt;/p&gt;

&lt;p&gt;First, I’m using a NodeJS env. It’s the most intuitive for me plus it keeps JS across all.&lt;br&gt;
For the app server I’m using &lt;a href="https://fastify.dev/" rel="noopener noreferrer"&gt;Fastify&lt;/a&gt;. The templating engine is &lt;a href="https://ejs.co/" rel="noopener noreferrer"&gt;EJS&lt;/a&gt;. The client would be plain ‘ol HTML, JS and CSS with the help of &lt;a href="https://htmx.org/" rel="noopener noreferrer"&gt;HTMX&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That’s it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The page’s parts
&lt;/h3&gt;

&lt;p&gt;It was tempting to divide the page into little components and treat each as a template, but this is exactly where I found myself needing to insist on taking a different approach. &lt;br&gt;
It narrows down to HTMX ability to perform an AJAX call instead of making a “conventional” http GET request to render the app. &lt;br&gt;
While an http GET will refresh the entire page, causing all the resources to be re-fetched, the AJAX call allows me to replace a certain portion of the doc without the need for refresh, but this requires some architectural mind-shift.&lt;/p&gt;

&lt;p&gt;The diagram below represents the application’s parts -&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%2Fuztnyjn5xedenj5p4gam.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%2Fuztnyjn5xedenj5p4gam.png" alt="Image description" width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s go one by one and understand what each template is responsible for - &lt;/p&gt;
&lt;h4&gt;
  
  
  index.ejs
&lt;/h4&gt;

&lt;p&gt;This template is what's returned when we request the page from the root url. It is responsible for loading the app’s CSS and JS required, and inside of it it embeds the todos.ejs.&lt;br&gt;
Notice that the input for “What needs to be done?” is part of the index.ejs since there is no need for it to be refreshed from the server. &lt;/p&gt;
&lt;h4&gt;
  
  
  todos.ejs
&lt;/h4&gt;

&lt;p&gt;This is the main application. It holds the Todos list and the footer at the bottom.&lt;br&gt;
I wanted to bundle them together since this is the part which gets refreshed whenever we add, remove, toggle or even filter todos.&lt;/p&gt;

&lt;p&gt;This template is also the one which has the hx-triggers to load the data when events are triggered from the server. Basically, this is the place which listens to what happens in the application and refreshes the application part accordingly. &lt;/p&gt;
&lt;h4&gt;
  
  
  todo-list.ejs
&lt;/h4&gt;

&lt;p&gt;This is the todo list, a simple UL with a loop going over the given todos and creating an LI element for each.&lt;br&gt;
In the original TodoMVC template, this section is also responsible for rendering the “toggle-all” button, found on the left side of the input. I wanted to keep this structure, though I’d probably not constructed it this way to begin with. &lt;/p&gt;

&lt;p&gt;The “toggle-all” button is using &lt;code&gt;hx-patch&lt;/code&gt; to make a PATCH request to the &lt;code&gt;/todos/toggle&lt;/code&gt; endpoint in order to toggle all the todos.&lt;/p&gt;
&lt;h4&gt;
  
  
  footer.ejs
&lt;/h4&gt;

&lt;p&gt;This template is responsible for the todo count, filtering, and clearing all completed todos. It only appears when there are todos in the system and updates whenever a todo is added, deleted, or marked as completed or active.&lt;br&gt;
It uses anchor tags for the filters, navigating to a URL that includes the filter. However, we’re using HTMX boosting here to ensure that we don’t refresh the entire page, but only fetch what is needed.&lt;/p&gt;
&lt;h4&gt;
  
  
  single-todo.ejs
&lt;/h4&gt;

&lt;p&gt;This one renders a single todo, but it is not that simple.&lt;br&gt;
You can toggle the single todo by clicking on the checkbox, and you can also edit the todo’s label by double-clicking it.&lt;br&gt;
I’m using &lt;code&gt;hx-patch&lt;/code&gt; to make a PATCH request to the &lt;code&gt;/todos/toggle/:id&lt;/code&gt; endpoint (now with the corresponding todo’s id), and I’m using hx-patch also to make a PATCH request to the &lt;code&gt;/todos/edit/:id&lt;/code&gt; endpoint to edit the label.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rendering &amp;amp; boosting
&lt;/h3&gt;

&lt;p&gt;HTMX, if you allow it, makes you think of an application rendering in 2 ways - either you refresh the entire doc upon state change or you swap different sections in the page according to a server state change.&lt;br&gt;
This means that you need to design the document content in such a way that you can support both methods.&lt;/p&gt;

&lt;p&gt;Our application has a &lt;code&gt;hx-boost="true"&lt;/code&gt; attribute at its body level. This means that it will be applied to all nested in it, since this attribute is inherited. &lt;/p&gt;

&lt;p&gt;In TodosMVC case it can be shown when requesting the application for the first time in comparison to fetching just the internal todos markup (that is the todos list and footer)&lt;/p&gt;
&lt;h4&gt;
  
  
  Accessing the app for the first time
&lt;/h4&gt;

&lt;p&gt;To get a better understanding, here is a sequence diagram of fetching the application for the first time:&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%2F28tls2hcb76we8f32vlg.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%2F28tls2hcb76we8f32vlg.png" alt="Image description" width="800" height="844"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When this is the first time the application gets rendered, the GET request is not boosted, meaning that we want to get the entire markup for the main document, and not just parts of it.&lt;/p&gt;

&lt;p&gt;HTMX knows how to append a hx-boosted header on requests which are boosted and it is something we can query later on the server and know which action should be performed. As can seen in the example below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;fastify&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="s1"&gt;/&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;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&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;let&lt;/span&gt; &lt;span class="nx"&gt;isHxBoosted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hx-boosted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&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;markup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isHxBoosted&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderTodos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&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="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoController&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renderIndexPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&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;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;'&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="nx"&gt;markup&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;In the example above we render the entire index page and return it back to the browser.&lt;/p&gt;

&lt;h4&gt;
  
  
  Filtering the todos
&lt;/h4&gt;

&lt;p&gt;Let’s look at what happens when we filter the todos by clicking the “completed” filter button on the Footer:&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%2Fht6xkouaer5u3pt9wan4.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%2Fht6xkouaer5u3pt9wan4.png" alt="Image description" width="800" height="687"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We perform a GET request with the filter set to “completed”. The request is boosted and the server knows not to do a completed render to the page, but just the todos part. It renders the todos.ejs template with the filtered data and returns this markup to the client, which knows to swap it in the right place.&lt;/p&gt;

&lt;h4&gt;
  
  
  Editing a single todo
&lt;/h4&gt;

&lt;p&gt;What happens when we edit a single todo item?&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%2Fxb54f7ggjfoknmclf0kh.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%2Fxb54f7ggjfoknmclf0kh.png" alt="Image description" width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example above we send a PATCH request (yes, we try to stick to ReST protocols the best we can) with the new label and the todo item’s id, from there we update the todo and render a single todo item, by rendering the single-todo.ejs template. Once we have its markup we return it to the client which knows to swap it in the closest LI element to the element which triggered this request. &lt;/p&gt;

&lt;h4&gt;
  
  
  Adding a new todo
&lt;/h4&gt;

&lt;p&gt;And when we’re adding a new todo? Here we’re using the HTMX events - &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%2F427pxztjrnq6pow07uf6.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%2F427pxztjrnq6pow07uf6.png" alt="Image description" width="800" height="998"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the example above we use the great power of HTMX events. &lt;br&gt;
The first action is easy to understand, we send a POST request for the new label to be added to the todos list.&lt;br&gt;
Once this task is completed, we do not return any markup, but rather return a response that has a hx-trigger header with an event called “todoCreated”.&lt;br&gt;
This event can then be listened upon on the client, and when it receives it can perform actions, like refreshing the todos.&lt;br&gt;
Registering to this event is done on the todos.ejs template where, as you can see, we also listen to other events&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt;
   &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"todos"&lt;/span&gt;
   &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/todos?filter=&amp;lt;%= filter %&amp;gt;"&lt;/span&gt;
   &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"todoCreated from:body, todoDeleted from:body, allToggled from:body, singleToggled from:body"&lt;/span&gt;
   &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"outerHTML"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In most of the cases, there is a need to fetch the entire inner todos markup, that is the todos list and the footer. The most optimized way is to fetch it all in a single request instead of separating it to several requests, each for a different part of the app. See more about it in the “Thoughts” parts below.&lt;/p&gt;

&lt;h4&gt;
  
  
  Toggling and cleaning
&lt;/h4&gt;

&lt;p&gt;Both are done with the same techniques shown above. You can check out the code on GitHub to see how it is done, &lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering
&lt;/h3&gt;

&lt;p&gt;The footer holds a filtering section where you can choose between “Active”, “Completed” and “All” filters.&lt;br&gt;
The way it is done is by navigating to a URL which holds the filter type as a request param. The A href looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"selected"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"?filter=active"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Active&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But as you guessed it we do not want a full page reload when a filter is selected, and again the &lt;code&gt;hx-boost&lt;/code&gt; comes to our help and the request for the “index” page is boosted and our server knows how to handle this and returns only the relevant markup.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;footer.ejs&lt;/code&gt; we set that the response target will be the .todos selector and we’re set to go:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"filters"&lt;/span&gt; &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;".todos"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that’s it. We have a fully working TodoMVC app built with HTMX :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Thoughts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Client side logic
&lt;/h3&gt;

&lt;p&gt;At certain places in the client code I found myself in need to add JS code, using “real” vanilla JS in one place and HTMX syntax in another.&lt;br&gt;
One example is clearing the input for new todos when a submit for a new todo was made. It looks like this:&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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Clear the todo label input after creating a new todo&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todoCreated&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="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;todoLabelInputElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.new-todo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;todoLabelInputElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&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="p"&gt;})(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, I could have done this by returning a blank todo from the server upon creating a new todo, but I thought it was an overkill (WDYT?). &lt;/p&gt;

&lt;p&gt;There is no rule to avoid using JS on the client, and you should not avoid that when it makes sense, but I think that the approach should be “JS should be used to enrich/hydrate the markup we receive from the server and not for creating the DOM in the first place”.&lt;br&gt;
I’m cool with the decision of performing this cleanup in JS on the client side.&lt;/p&gt;
&lt;h3&gt;
  
  
  Switching styles upon editing
&lt;/h3&gt;

&lt;p&gt;When you double click a single todo item, it changes style class to “editing” and also when you blur the input it switches back. This transition is done by listening for those events using the hx-on syntax and then executing the event handler with inline JS. Yikes… I know.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;hx-on:dblclick=&lt;/span&gt;&lt;span class="s"&gt;"this.closest('li').classList.replace('&amp;lt;%= todo.status %&amp;gt;', 'editing');this.closest('li').querySelector('.edit').focus();"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;todo.label&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the input, check the &lt;code&gt;hx-on:blur&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt;
       &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"edit"&lt;/span&gt;
       &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"label"&lt;/span&gt;
       &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;%= todo.label %&amp;gt;"&lt;/span&gt;
       &lt;span class="na"&gt;hx-on:blur=&lt;/span&gt;&lt;span class="s"&gt;"this.closest('li').classList.replace('editing', '&amp;lt;%= todo.status %&amp;gt;');"&lt;/span&gt;
       &lt;span class="na"&gt;hx-patch=&lt;/span&gt;&lt;span class="s"&gt;"/todos/edit/&amp;lt;%= todo.id %&amp;gt;"&lt;/span&gt;
       &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"closest li"&lt;/span&gt;
       &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"outerHTML"&lt;/span&gt;
   &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not very elegant, but I feel that as long as it is kept at a reasonable scope,it’s fine. What bothers me a bit is that we’re keeping a state (the todo.status) on the client, and one of the things I like about HTMX is the fact that it &lt;del&gt;forces&lt;/del&gt; encourages you to keep the state in a single place - the server.&lt;br&gt;
Having said that, calling the server each time to change the style is a bit of an overkill. If you have suggestions on how this can be more elegant while still performant, do share in the comments below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Requesting the entire todos markup each time?
&lt;/h3&gt;

&lt;p&gt;For quite a few scenarios I’m requesting the server to render the todos markup, which consists of the todos list and footer, over and over again.&lt;br&gt;
Even with the HTMX boosting it feels a bit too much… but is it?&lt;/p&gt;

&lt;p&gt;Yes, reactive applications, which do some heavy lifting on the client, know to listen to changes and render the specific elements accordingly, and when I look at the DOM changes I kinda wish I could do the same with HTMX.&lt;/p&gt;

&lt;p&gt;Well, in theory, I could. &lt;br&gt;
I could separate the changes into different http requests, but it felt like going too “religious” over this. Yes, the application core renders again and we have a request going out of the browser again, but receiving a markup which represents the single source of state found on the server kinda makes me peaceful about it. It is not like we make a request to the server for an initial state and from there on we’re juggling between 2 potential states, one on the client and one on the server, hoping that they will be synced in the end. &lt;br&gt;
Don’t know… still thinking about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Going offline
&lt;/h3&gt;

&lt;p&gt;I think that this is the biggest concern here.&lt;br&gt;
If the application is offline, nothing can render as expected on the client. It is not like we fetched the entire code needed for the client and from there on we can still perform actions on the client and buffer them in case of offline network. Here it is a bit more complicated. &lt;br&gt;
There are ways to mitigate it using cache techniques or service-workers, but I think that when you choose HTMX you need to understand that your client is highly dependent on network connection. &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;This was a really interesting challenge. I learned quite a lot on how HTMX works and what it means to create an architecture for an application using this technology. &lt;br&gt;
I find HTMX very appealing but I understand from what I read &lt;a href="https://htmx.org/essays/why-gumroad-didnt-choose-htmx/" rel="noopener noreferrer"&gt;out&lt;/a&gt; &lt;a href="https://htmx.org/essays/htmx-sucks/" rel="noopener noreferrer"&gt;there&lt;/a&gt; that there are still places where HTMX fails. I’m not entirely convinced yet, since I think that most of these failures derive from trying to approach HTMX applications in the same way you would with Reactive technology. It’s not. &lt;br&gt;
This implementation is very naive, but it is a good start to understand the potential of HTMX and also where it falls short. I think that the integration with WebComponents can be an interesting evolution to this project.&lt;br&gt;
I really like the idea that HTMX strips down much of the complexity of building a web app these days. &lt;/p&gt;

&lt;p&gt;As said the code can found here: &lt;a href="https://github.com/mbarzeev/todomvc/tree/htmx/examples/htmx" rel="noopener noreferrer"&gt;https://github.com/mbarzeev/todomvc/tree/htmx/examples/htmx&lt;/a&gt; &lt;br&gt;
You can also check out the PR I’ve made on the original TodoMVC repo, and I’d really appreciate you voting for it so that the maintainers will review and consider including it (even when the repo is no longer actively maintained).&lt;/p&gt;

&lt;p&gt;Hope this helps and inspires you,&lt;br&gt;
Take care&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://hypermedia.systems/" rel="noopener noreferrer"&gt;HyperMedia Systems book&lt;/a&gt;&lt;br&gt;
&lt;a href="https://htmx.org/docs/" rel="noopener noreferrer"&gt;HTMX docs&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/tastejs/todomvc" rel="noopener noreferrer"&gt;TodoMVC on Github&lt;/a&gt;&lt;br&gt;
&lt;a href="https://c4model.com/" rel="noopener noreferrer"&gt;C4Model&lt;/a&gt;&lt;/p&gt;

</description>
      <category>htmx</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>node</category>
    </item>
    <item>
      <title>Back to sq1: My basic HTML template</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Tue, 01 Oct 2024 10:00:00 +0000</pubDate>
      <link>https://dev.to/mbarzeev/back-to-sq1-my-basic-html-template-4kbb</link>
      <guid>https://dev.to/mbarzeev/back-to-sq1-my-basic-html-template-4kbb</guid>
      <description>&lt;p&gt;What is the best basic template for an HTML doc?&lt;br&gt;
I often ask myself this question, and though there are quite a few code generators which do just that (or at least claim to) I wanted to take control over the very basic foundation of any web page, regardless of whether I’m using React, Solid, vanilla or any other frontend solution to build it.&lt;br&gt;
What should it include and why?&lt;/p&gt;

&lt;p&gt;The core idea behind "back to sq1" is to utilize the right tools for the right tasks and to fully understand the purpose of each tool. As we go along this post, I hope this will become clearer. I will go through each line (or related group of elements) step by step, explaining its function and why I believe it should be part of a basic HTML template.&lt;/p&gt;



&lt;p&gt;Let’s start from the top - &lt;/p&gt;
&lt;h2&gt;
  
  
  DOCTYPE
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;According to the specs “Including the DOCTYPE in a document ensures that the browser makes a best-effort attempt at following the relevant specifications.” We don’t want anything surprising going on when the document is rendered, right?&lt;br&gt;
BTW, it is case insensitive, so we can also write &lt;code&gt;&amp;lt;!DOCTYPE HTML&amp;gt;&lt;/code&gt; but I prefer the format above (so is my IDEA so we’re cool on that).&lt;/p&gt;
&lt;h2&gt;
  
  
  The html root element
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="na"&gt;dir=&lt;/span&gt;&lt;span class="s"&gt;"ltr"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    …
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For creating our HTML doc we need to start with the html tag. Did you know that you can omit this tag in certain situations? But why would we - let’s keep it readable and understandable.&lt;br&gt;
The lang attribute can be applied to all elements  (yes, not just html), but it is a good practice to put the lang up there and have all its descendant elements adhere to it.&lt;/p&gt;

&lt;p&gt;We also want to add the direction the page is at, meaning what is the direction of the content in this page. E.g. If it's English, we will use ltr (left to right), if it's Hebrew, rtl.&lt;/p&gt;

&lt;p&gt;That pretty much sums it for the html root tag. The other attributes available for it are either obsolete or not needed for common modern web pages.&lt;/p&gt;
&lt;h2&gt;
  
  
  The head element
&lt;/h2&gt;

&lt;p&gt;The head element is where things begin to vary. Here we need to think about what we want this tag to contain as it can affect our page performance among other things. &lt;br&gt;
We will start with the page’s metadata. The head tag will usually hold some meta tags. Lets see what are the basic meta tags we would like to have&lt;/p&gt;
&lt;h3&gt;
  
  
  Meta tags
&lt;/h3&gt;
&lt;h4&gt;
  
  
  charset
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Here we set the encoding of the entire html doc. It must be utf-8 since it is the only valid encoding for HTML5 documents. Also it has to be within the first 1024 char of the document, so it is wise to put it first.&lt;br&gt;
Having this will eliminate the need to use the content-type http-equiv meta tag (see below).&lt;/p&gt;
&lt;h4&gt;
  
  
  viewport
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The name and the content are like “key” and “value”. Here we set the viewport to width=device-width, initial-scale=1.0. What do they mean?&lt;br&gt;
“width=device-width” means that when the page loads, its width should be the width of the device it is being displayed on. You can also set this width to actual size in pixels but I don’t think there is a real use-case for that.&lt;br&gt;
The “initial-scale” defines the zoom level of the document. 1.0 means that it is 100%.&lt;br&gt;
So basically what we’re saying here is “your width is the display width and your zoom should be 100% to begin with”.&lt;/p&gt;
&lt;h4&gt;
  
  
  http-equiv
&lt;/h4&gt;

&lt;p&gt;“Defines a pragma directive”. What is a pragma you ask? Basically it is more information on how to handle this document that the browser understands. &lt;br&gt;
The allowed values for this meta are http headers. According to the docs there are 5 headers you can use, but I’m only interested in one - the content-security-policy.&lt;br&gt;
The content-security-policy is the one that allows us to define the security policy without needing to register them on the response header.&lt;br&gt;
There are some advantages to that, one of them relates to the JAMstack, where you eventually serve static pages from a CDN. It might be that the pages being cached do not bear the same headers the response has from the origin server and you still want your CSP (content security policy) to protect the page. In addition to that, headers tend to have size limits and sometimes CSPs can be quite large.&lt;br&gt;
So I suggest having this meta in your html page and not on the header. I think that the most basic one is allowing src resources only from a secured http connection. Later on you can enrich that to gain better security over the web page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"default-src https:"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Author
&lt;/h4&gt;

&lt;p&gt;This one is for setting the author of this document, and why the hell not to?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Matti Bar-Zeev"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Description
&lt;/h4&gt;

&lt;p&gt;This is mainly for SEO. It describes the page purpose so that search engine indexing could get a better understanding on how to index it. There are a lot more meta tags and tricks to increase SEO indexing but this is a topic which deserves its own post. For now, here is what I have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Your site description"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Title tag
&lt;/h3&gt;

&lt;p&gt;Finally we’re getting into something which also has a displayable aspect to it - the title tag.&lt;br&gt;
This tag determines the text shown in a page's tab. Let’s keep it as simple as it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Your Website Title&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Fav Icon
&lt;/h3&gt;

&lt;p&gt;Another important thing is the fav icon. This is the icon that the browser will show on the page’s tab, and in bookmarks (hence the “favorite” in the name) and usually bears the brand logo.&lt;br&gt;
Usually this will hold a 16x16 pixels .png, .ico or some other low volume image. avoid large fav icons since they still need to be seen in a 16x16 resolution yet their size will affect your site’s Core Web Vitals (CWV) score. The path can be either relative or absolute. I will go with the relative one&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./favicon.ico"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resetting the styles
&lt;/h3&gt;

&lt;p&gt;It is well known that each browser has its own “understanding” of what the default CSS styles should be, thus the same CSS can result in a different visual experience across different browsers. So far for a single standard to rule them all. &lt;br&gt;
What we need to do to avoid this chaos is to reset and align the styles before we start writing our first CSS rule.&lt;br&gt;
There is a great article by &lt;a href="https://www.joshwcomeau.com/" rel="noopener noreferrer"&gt;Josh Cameo&lt;/a&gt; (if you haven't already, check out his blog, the guy's a true inspiration) where he gives &lt;a href="https://www.joshwcomeau.com/css/custom-css-reset/" rel="noopener noreferrer"&gt;his reset style rules&lt;/a&gt; with a good explanation about each decision. This is my goto reset CSS file.&lt;br&gt;
I’m adding the media=”all” to declare that this reset is applied to all sorts of media, no matter screen size.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./reset.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other CSS resources
&lt;/h3&gt;

&lt;p&gt;I’m going to separate my CSS resources. We already saw that we have the reset.css which aligns all browsers. On top of that I will have a common.css which will contain rules that are common for all pages in the site, and on top of that I will have the specific page.css that will contain rules only for the page we’re at.&lt;br&gt;
This gives a lot of advantages, first by caching the “static” css files while only fetching the changing CSS for each page, and also keeping a good SoC of which CSS rule goes where, and not mixing responsibilities.&lt;br&gt;
So I’m adding these lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./common.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./page.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The body element
&lt;/h2&gt;

&lt;p&gt;This is where our page content is being placed. Generally speaking this tag should have 3 main “sections” - header, main and footer. These are all semantic html tags, and here is the place to emphasize the importance of using semantic html over generic div’s. This will help SEO and users who need a better accessible site. Always try to find the most suitable semantic html to represent your content.&lt;/p&gt;

&lt;p&gt;At the end of the content inside the body element I will put the scripts. The reason is well known but still - JS scripts tend to be render blocking, and also cause the browser to fetch, evaluate and execute them (which may take time). Having the scripts at the end gives the browser a chance to render the content before starting to execute scripts. &lt;br&gt;
So here what we have in the body element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;Header&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;Main&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;Footer&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./page.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I think that this is it for now. Here is the final result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"default-src https:"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"author"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Matti Bar-Zeev"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"Your site description"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Your Website Title&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./favicon.ico"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./reset.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./common.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"./page.css"&lt;/span&gt; &lt;span class="na"&gt;media=&lt;/span&gt;&lt;span class="s"&gt;"all"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;


   &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;Header&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;Main&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;Footer&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;


       &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./page.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can start enriching your web page with content and interactivity.&lt;br&gt;
I hope that this basic template helps you understand the different parts better so you can choose them with care instead letting a generator do the decisions for you, without knowing their meaning&lt;/p&gt;

&lt;p&gt;If you have any other ideas that you think should be included in the basic HTML template, please leave your comment in the comments section below&lt;/p&gt;

&lt;p&gt;Cheers&lt;/p&gt;




&lt;p&gt;*Hey! for more content like the one you've just read check out &lt;a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" rel="noopener noreferrer"&gt;@mattibarzeev&lt;/a&gt; 🍻&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@seanstratton?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Sean Stratton&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/black-stacking-stones-on-gray-surface-ObpCE_X3j6U?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>html</category>
      <category>coding</category>
    </item>
    <item>
      <title>Optimize your React’s Carousel with MediaLoader</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Sat, 23 Mar 2024 15:06:33 +0000</pubDate>
      <link>https://dev.to/mbarzeev/optimize-your-reacts-carousel-with-medialoader-c90</link>
      <guid>https://dev.to/mbarzeev/optimize-your-reacts-carousel-with-medialoader-c90</guid>
      <description>&lt;p&gt;At some stage in our development journey, we've likely come across an image carousel. This component typically contains multiple images nested within it, allowing users to navigate forwards and backwards between them.&lt;/p&gt;

&lt;p&gt;However, there's a performance drawback to many existing implementations. Consider an image carousel featuring 100 images: in most scenarios, all images are loaded upfront, even if the user only views a single image. This results in a significant waste of network bandwidth and may delay the retrieval and availability of other critical assets.&lt;/p&gt;

&lt;p&gt;Let me show you what I mean -&lt;br&gt;
I’m using the &lt;a href="https://github.com/leandrowd/react-responsive-carousel" rel="noopener noreferrer"&gt;react-responsive-carousel&lt;/a&gt; as my React carousel component and I'm adding six images to it. Here's the code for 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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Carousel&lt;/span&gt; &lt;span class="nx"&gt;showThumbs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{false}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;carousel&lt;/span&gt;&lt;span class="dl"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-01.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-02.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-03.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-04.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-05.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-06.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Carousel&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As demonstrated in the video below, upon rendering the carousel, all six images are requested and fetched, even though only the first one is initially visible. Subsequently, I can proceed to navigate between the images:&lt;/p&gt;

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

&lt;p&gt;Now, let's introduce the &lt;a href="https://www.npmjs.com/package/@pedalboard/media-loader" rel="noopener noreferrer"&gt;MediaLoader&lt;/a&gt; to assist us with the loading strategy. I'm merely wrapping the Carousel component with the &lt;a href="https://www.npmjs.com/package/@pedalboard/media-loader" rel="noopener noreferrer"&gt;MediaLoader&lt;/a&gt; and specifying a loading strategy that operates as follows: it examines the transform style of the sliding element and loads the necessary image accordingly. This approach is straightforward yet effective for this particular scenario. Take a look at the updated code below:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MediaLoader&lt;/span&gt;
    &lt;span class="nx"&gt;loadingStrategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadMedia&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;sliderElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.slider.animated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;imageIndexToLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getImageIndexFromTransformStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sliderElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&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;getImageIndexFromTransformStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;transformStyle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;imageIndexToLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transformStyle&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;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;transformStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/-&lt;/span&gt;&lt;span class="se"&gt;?[\d&lt;/span&gt;&lt;span class="sr"&gt;.&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;%|px&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&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;match&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;transformPercent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                    &lt;span class="nx"&gt;imageIndexToLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transformPercent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;100&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="nx"&gt;imageIndexToLoad&lt;/span&gt;&lt;span class="p"&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;loadImageByIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;imageRefToLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;index&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;imageRefToLoad&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;imageRefToLoad&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="nf"&gt;loadMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageRefToLoad&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="p"&gt;}&lt;/span&gt;


        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutationList&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;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;mutation&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;mutationList&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributeName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mutation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                    &lt;span class="nx"&gt;imageIndexToLoad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getImageIndexFromTransformStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="nf"&gt;loadImageByIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageIndexToLoad&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;break&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&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;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sliderElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;


        &lt;span class="nf"&gt;loadImageByIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageIndexToLoad&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;Carousel&lt;/span&gt; &lt;span class="nx"&gt;showThumbs&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;carousel&lt;/span&gt;&lt;span class="dl"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-01.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-02.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-03.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-04.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-05.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-06.jpg&lt;/span&gt;&lt;span class="dl"&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;p&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;legend&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Legend&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Carousel&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MediaLoader&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that there have been no alterations made to the Carousel code itself. As depicted in the video below, only the necessary image is now loaded:&lt;/p&gt;

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

&lt;p&gt;Pretty neat, right? You can also check the working demo in the &lt;a href="https://master--65f0a7c612ec612e3b7b2059.chromatic.com/?path=/docs/medialoader--docs" rel="noopener noreferrer"&gt;project’s Storybook&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Naturally, you can refine the logic and make it more sophisticated. For instance, you could consider loading the next image upon hovering over the arrow button. Additionally, you might opt to load images exclusively during periods of network idleness. The decision is entirely yours, granting you full control over the process.&lt;/p&gt;

&lt;p&gt;To read more about the MediaLoader see &lt;a href="https://dev.to/mbarzeev/take-control-over-your-media-loading-kjl"&gt;this post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Share your thoughts in the comment :) &lt;/p&gt;

</description>
      <category>react</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>performance</category>
    </item>
    <item>
      <title>Take control over your media loading</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Tue, 19 Mar 2024 17:00:00 +0000</pubDate>
      <link>https://dev.to/mbarzeev/take-control-over-your-media-loading-kjl</link>
      <guid>https://dev.to/mbarzeev/take-control-over-your-media-loading-kjl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MediaLoader&lt;/strong&gt; is a versatile React component that provides fine-grained control over the loading of media assets such as images, videos, and audio files. It offers a customizable loading strategy, allowing developers to prioritize resources and optimize user experience.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;It has been a while, I know.&lt;br&gt;
Hopefully I have something for you that will compensate for my absence 🙂&lt;/p&gt;

&lt;p&gt;As you may already know, web performance has become increasingly significant within the frontend community. Those who value their work and prioritize user experience dedicate considerable effort to enhancing the performance of their websites and applications.&lt;/p&gt;

&lt;p&gt;Improving performance involves loading only essential resources, allowing the network connection to prioritize critical assets and expedite the page's readiness and interactivity.&lt;br&gt;
Media content—such as images, videos, and audio—typically constitutes the bulk of data to be loaded. Moreover, when the browser encounters a media tag during document parsing, it initiates immediate loading by default.&lt;/p&gt;

&lt;p&gt;The "lazy" attribute for the img tag aims to alleviate network load by enabling the deferred loading of images until necessary. When you use the lazy attribute, the browser will only load the image when it enters the viewport, helping to conserve bandwidth and improve page loading times, especially on pages with many images.&lt;/p&gt;

&lt;p&gt;However, this is insufficient. We recognize that viewport entry alone may not always suffice as the desired trigger. There are instances where we might prefer loading based on observing a CSS property or clicking an element, among other examples. Additionally, it's important to note that images are not the sole type of media content we aim to lazy-load, yet they unfortunately lack support for such functionality.&lt;/p&gt;

&lt;p&gt;This is where the &lt;a href="https://www.npmjs.com/package/@pedalboard/media-loader" rel="noopener noreferrer"&gt;MediaLoader&lt;/a&gt; comes in to fill the gap. &lt;/p&gt;

&lt;p&gt;MediaLoader is a versatile React component that provides fine-grained control over the loading of media assets such as images, videos, and audio files. It offers a customizable loading strategy, allowing developers to prioritize resources and optimize user experience.&lt;/p&gt;

&lt;p&gt;With MediaLoader, you can effortlessly manage the loading process, dynamically loading media content based on user interactions, viewport visibility and much more. Whether you're building a gallery, a multimedia-rich website, or an application with heavy media content, MediaLoader empowers you to deliver a seamless and performant user experience.&lt;/p&gt;

&lt;p&gt;MediaLoader is designed to be as non-intrusive as possible, integrating into your existing React applications without imposing a heavy footprint and without changing the DOM structure. It provides a lightweight wrapper around your current media, without requiring a complete overhaul of your codebase.&lt;/p&gt;

&lt;p&gt;You can check out some demo examples in this &lt;a href="https://65f0a7c612ec612e3b7b2059-pqygczjhcu.chromatic.com/" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;, and see the code in the &lt;a href="https://github.com/mbarzeev/pedalboard/tree/master/packages/media-loader" rel="noopener noreferrer"&gt;pedalboard repo&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  How do I use it?
&lt;/h2&gt;

&lt;p&gt;You first need to 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;yarn add @pedalboard/media-loader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @pedalboard/media-loader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, say you have this JSX:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-04.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-04&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-05.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-06.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-06&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-09.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-09&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All you need to do is to wrap it with the MediaLoader component and provide a loading strategy to it, like so:&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="nx"&gt;MediaLoader&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;@pedalboard/media-loader&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;MediaLoader&lt;/span&gt;
    &lt;span class="nx"&gt;loadingStrategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadMedia&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="c1"&gt;// You loading strategy here&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;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-04.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-04&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-05.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-05&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-06.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-06&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-09.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-09&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MediaLoader&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loadingStrategy is a function which receives 2 arguments - An array of the media element refs and the loadMedia function. When you call the loadMedia function with a given ref it will load the associated media for it, whether it is an image, video or audio.&lt;/p&gt;

&lt;p&gt;Your loading strategy might be something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;loadingStrategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadMedia&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;intersectionCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entries&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;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&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;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isIntersecting&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                                   &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;elem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                                   &lt;span class="nf"&gt;loadMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elem&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="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                           &lt;span class="na"&gt;rootMargin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="p"&gt;};&lt;/span&gt;
                       &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;observer&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;IntersectionObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intersectionCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


                       &lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mediaHTMLElementRef&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;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mediaHTMLElementRef&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                               &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;In the example above we act the same as the lazy attribute of an img tag. Once the element intersects the viewport, we trigger the load.&lt;/p&gt;

&lt;p&gt;You can now probably see the great control it allows you to have over your media content loading. Another example can be monitoring the left CSS property of the element and only when it surpasses 300, load it’s media 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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;MediaLoader&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;@pedalboard/media-loader&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;lt;&lt;/span&gt;&lt;span class="nx"&gt;MediaLoader&lt;/span&gt;
    &lt;span class="nx"&gt;loadingStrategy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadMedia&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;monitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;computedStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentElement&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;currentLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;computedStyle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;left&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// If the image container reach a certain left then load it&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;currentLeft&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;loadMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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="c1"&gt;// Continue monitoring in the next frame&lt;/span&gt;
            &lt;span class="nf"&gt;requestAnimationFrame&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;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="nx"&gt;mediaHTMLElementRefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;mediaHTMLElementRef&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;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mediaHTMLElementRef&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;requestAnimationFrame&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;monitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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="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="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLDivElement&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="s1"&gt;animate-media&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;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-01.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-09&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&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;And&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLDivElement&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="s1"&gt;animate-media&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;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-02.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-09&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&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;Quick&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nx"&gt;Click&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="p"&gt;;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&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="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-container&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="nx"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLDivElement&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="s1"&gt;animate-media&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;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/image-03.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image-09&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/img&lt;/span&gt;&lt;span class="err"&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;Click&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="p"&gt;;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/MediaLoader&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can read more about it in the project &lt;a href="https://github.com/mbarzeev/pedalboard/tree/master/packages/media-loader#readme" rel="noopener noreferrer"&gt;README file&lt;/a&gt;, download it from the &lt;a href="https://www.npmjs.com/package/@pedalboard/media-loader" rel="noopener noreferrer"&gt;NPM registry&lt;/a&gt; and start playing with it 🙂&lt;/p&gt;

&lt;p&gt;I’d love to hear your thoughts and feedback on this project, and if it helped you produce a better performing web content.&lt;/p&gt;

&lt;p&gt;Until next time,&lt;br&gt;
Cheers!&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/@usgs?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;USGS&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/a-red-blue-and-green-fluid-painting-on-a-black-background-hoS3dzgpHzw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>performance</category>
      <category>webdev</category>
      <category>react</category>
      <category>javascript</category>
    </item>
    <item>
      <title>SolidStart, Netlify and Forms</title>
      <dc:creator>Matti Bar-Zeev</dc:creator>
      <pubDate>Fri, 23 Jun 2023 11:14:08 +0000</pubDate>
      <link>https://dev.to/mbarzeev/solidstart-netlify-and-forms-3ang</link>
      <guid>https://dev.to/mbarzeev/solidstart-netlify-and-forms-3ang</guid>
      <description>&lt;p&gt;My daughter has ventured into the world of bracelet crafting, and now she's on a mission to conquer the internet with her handmade creations and maybe even get some orders going. I thought it was a good opportunity to have a go on &lt;a href="https://start.solidjs.com/getting-started/what-is-solidstart" rel="noopener noreferrer"&gt;SolidStart&lt;/a&gt; and build a complete (yet very naive) eComm site with it.&lt;/p&gt;

&lt;p&gt;You may wonder why I chose SolidStart. Well, I am particularly impressed with SolidJS and believe that this framework offers a compelling alternative to other UI frameworks out there, mainly React. However, that is a separate discussion altogether.&lt;/p&gt;

&lt;p&gt;Fueled by an abundance of motivation and under the watchful eye of my sweet yet demanding child, I embarked on my late-night coding sessions, completely unaware of the intriguing (or should I say, "frustrating") challenges that awaited me.&lt;/p&gt;

&lt;p&gt;Curious to know how I conquered those obstacles? Well, you're in for a treat. Just keep reading...&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Hey! for more content like the one you're about to read check out &lt;a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" rel="noopener noreferrer"&gt;@mattibarzeev&lt;/a&gt; on Twitter&lt;/em&gt; 🍻&lt;/p&gt;




&lt;p&gt;First let’s set some background &lt;/p&gt;

&lt;h2&gt;
  
  
  SolidStart?
&lt;/h2&gt;

&lt;p&gt;SolidStart is a meta-framework (I didn’t coin the term, look it up) that stands shoulder-to-shoulder with the likes of NextJS, SvelteKit, and Remix. It's a game-changer when it comes to simplifying the development process of robust web applications. Think of it as your go-to solution for effortlessly bundling essential components like routing, authentication, and other features that are often deemed repetitive and mundane.&lt;/p&gt;

&lt;h2&gt;
  
  
  Netlify?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.netlify.com/" rel="noopener noreferrer"&gt;Netlify&lt;/a&gt; is a platform which helps developers publish their web content efficiently and fast. From efficient hosting solutions to seamless integration with your codebase, automation workflows, form submission handling, and powerful APIs, Netlify has got you covered and then some.&lt;/p&gt;

&lt;p&gt;What's particularly exciting is that Netlify has recently welcomed &lt;a href="https://dev.to/ryansolid"&gt;Rayn Corniato&lt;/a&gt;, the mastermind behind SolidJS, as one of its employees, so this all comes together quite nicely :)&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying SolidStart on Netlify
&lt;/h2&gt;

&lt;p&gt;I’ve created the site, with nested routes and what-have-you and reached a point where I want to deploy it and check the end-to-end “purchase” flow.&lt;br&gt;
I utilize Netlify's Github deployment option, which involves granting Netlify access to a specific Github repository. In this setup, I define a command, typically an npm script like &lt;code&gt;npm run build&lt;/code&gt;, that generates the site artifacts. Additionally, I specify the distribution directory.&lt;/p&gt;

&lt;p&gt;With this configuration, Netlify actively monitors any changes made to a designated branch on Github. When a change occurs, Netlify automatically triggers the build command and deploys the resulting artifacts from the specified distribution directory. This seamless integration ensures that my site stays up to date with minimal effort.&lt;/p&gt;

&lt;p&gt;This works well in most ordinary cases, but SolidStart is a bit different.&lt;br&gt;
You see, the “dist” directory does not contain any HTML files nor does it have any edge functions that handles the site’s requests. This means that if we give Netlify the out-of-the-box “dist” directory as the distribution directory, the site won’t work.&lt;/p&gt;

&lt;p&gt;Luckily there is a solution for that - A Vite plugin called &lt;a href="https://www.npmjs.com/package/solid-start-netlify/v/0.1.0" rel="noopener noreferrer"&gt;solid-start-netlify&lt;/a&gt;.&lt;br&gt;
Though still experimental, I found out that it did the work for me. You use it like this in your vite.config.js:&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="nx"&gt;netlify&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;solid-start-netlify&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;solid&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;netlify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;By setting the adapter like so, Vite will now create a new distribution directory called “netlify” and in it you will find the edge functions’ declaration that Netlify can work with in order to respond with the site’s content.&lt;br&gt;
Here is the &lt;code&gt;netlify&lt;/code&gt; directory content:&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%2Fotodqwngvdv94yr7su8o.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%2Fotodqwngvdv94yr7su8o.png" alt="Image description" width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can set Netlify to take the “netlify” directory as the distribution directory and it works as expected.&lt;/p&gt;
&lt;h2&gt;
  
  
  SolidStart forms submission on Netlify
&lt;/h2&gt;

&lt;p&gt;Netlify offers a cool feature that simplifies form submission handling. Here's how it works: by annotating your form as being managed by Netlify, you enable Netlify to automatically analyze the form, extract the necessary data, and attentively listen for any submissions. Once a form is submitted, you can conveniently access the data in a dedicated section within your Netlify admin console.&lt;/p&gt;

&lt;p&gt;But again, it’s different when it comes to SolidStart 🙂&lt;br&gt;
There are 2 challenges here - the first one is not related specifically to SolidStart (or SolidJS for that matter), but it is a general issue that Netlify has with JS generated forms.&lt;/p&gt;

&lt;p&gt;When I say JS generated forms I mean any form which is generated using JS (that obviously means JSX as well). You see, when netlify is “asked” to inspect a certain form, it expects it to be a real markup, so it can traverse it and extract the different fields that will be submitted. &lt;br&gt;
It cannot do that with JS generated form (yet, I believe this is something that they are either working on now or in the near future), so we have to use a workaround for that&lt;/p&gt;

&lt;p&gt;The workaround is to create an html file which has the form, named as  you desire, and with the same fields the JSX form has. Here is an example for the form tag of a “regular” html form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"checkout"&lt;/span&gt; &lt;span class="na"&gt;netlify&lt;/span&gt; &lt;span class="na"&gt;netlify-honeypot=&lt;/span&gt;&lt;span class="s"&gt;"bot-field"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“netlify” tells Netlify to inspect this form. I created such a file and placed it in the “public” directory. &lt;br&gt;
The next step is to take our JSX form and add a hidden input field to it which adds a “form-name” field to the submission with a value of the same name we gave the “regular” html form. &lt;br&gt;
So If we named our “regular” form “checkout” we need to add this field to our JSX form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"form-name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"checkout"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Netlify can inspect that “regular” html form and register it with its fields. You can read more about this approach &lt;a href="https://www.netlify.com/blog/2017/07/20/how-to-integrate-netlifys-form-handling-in-a-react-app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The second challenge is a bit more complex, and sadly not documented (so I hope you’ve found this resource after a short search) - &lt;br&gt;
I would like to have a &lt;code&gt;onSubmit&lt;/code&gt; callback for my form, cause I would like to do some data manipulations before sending it to the server, so I add this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit=&lt;/span&gt;&lt;span class="s"&gt;{handleSubmit}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And according to the docs, this is what you need to do in the callback:&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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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;myForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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;formData&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;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myForm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&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="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 gather to form data and do a POST request to the root route. &lt;br&gt;
But this does not submit the form… why?&lt;/p&gt;

&lt;p&gt;The reason  is that the root route, or any other SolidStart route for that matter, is handled by an edge function, and submitting a form to a route which is handled by an edge function cripples Netlify form submission handling. &lt;/p&gt;

&lt;p&gt;So what do we do then? We submit the form to a route we know is static, like… the “regular” html form we’ve used before (it’s in the public directory and therefore available). Changing the POST endpoint to that file solved the issue!&lt;/p&gt;

&lt;h2&gt;
  
  
  To recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Netlify does not know how to inspect JS generated forms out-of-the-box&lt;/li&gt;
&lt;li&gt;Make sure that you have a “regular” html form with a name and all the fields of the JSX form, available on the public directory&lt;/li&gt;
&lt;li&gt;Add hidden input in your JSX form which defines a “form-name” field with the value of the name you gave the “regular” html form&lt;/li&gt;
&lt;li&gt;You cannot submit a Netlify managed form to an endpoint which is handled by an edge function&lt;/li&gt;
&lt;li&gt;On submit, make sure that the endpoint you’re submitting to is not being handled by edge functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope this helps you, and if you’re aware of other or better ways to achieve that, make sure you share with the rest of us in the comments below :)&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Hey! for more content like the one you've just read check out &lt;a href="https://twitter.com/mattibarzeev?ref_src=twsrc%5Etfw" rel="noopener noreferrer"&gt;@mattibarzeev&lt;/a&gt; on Twitter&lt;/em&gt; 🍻&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/small&gt; &lt;/p&gt;

</description>
      <category>solidjs</category>
      <category>netlify</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
