<?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: Denner Vidal</title>
    <description>The latest articles on DEV Community by Denner Vidal (@dennervidal).</description>
    <link>https://dev.to/dennervidal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4196324%2F62f8b719-0804-4371-adae-b80d7dcfebdd.jpg</url>
      <title>DEV Community: Denner Vidal</title>
      <link>https://dev.to/dennervidal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dennervidal"/>
    <language>en</language>
    <item>
      <title>Even a mechanical migration deserves a spec</title>
      <dc:creator>Denner Vidal</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/dennervidal/even-a-mechanical-migration-deserves-a-spec-2ibo</link>
      <guid>https://dev.to/dennervidal/even-a-mechanical-migration-deserves-a-spec-2ibo</guid>
      <description>&lt;p&gt;Swapping Jest for Vitest is a mechanical migration. I still wrote a spec before touching a single test.&lt;/p&gt;

&lt;p&gt;The reason was simple: the design system already ran on Vite (Storybook included), and Jest with ts-jest kept a second transform pipeline alive just for tests. Consolidate, and get faster.&lt;/p&gt;

&lt;p&gt;The spec had three parts.&lt;/p&gt;

&lt;p&gt;Inventory. 68 test files. jest.fn in about 120 places across 30 files. jest.mock in 15. Fake timers in exactly one file. toBeInTheDocument used 219 times. I knew exactly what would break.&lt;/p&gt;

&lt;p&gt;Non-goals. No migrating legacy packages that have no tests. No root-level Vitest config until a second package gets tests. No swapping jsdom for happy-dom. And no behavior changes in the tests, only jest.* becoming vi.*.&lt;/p&gt;

&lt;p&gt;Definition of done. All 68 files passing with the same assertions, pnpm test and CI working unchanged, and five dependencies removed: jest, ts-jest, jest-environment-jsdom, @types/jest and ts-node.&lt;/p&gt;

&lt;p&gt;Result: one day of spec, one day of migration, five small, reviewable commits.&lt;/p&gt;

&lt;p&gt;The part that protects you most is the non-goals list. A mechanical migration turns into a rewrite the moment someone thinks "while I'm here...".&lt;/p&gt;

&lt;p&gt;What was the last "simple" migration that grew on your team?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>performance</category>
      <category>softwaredevelopment</category>
      <category>testing</category>
    </item>
    <item>
      <title>Monorepo tooling is a maintenance decision, not a technical one</title>
      <dc:creator>Denner Vidal</dc:creator>
      <pubDate>Sun, 08 Nov 2026 19:30:00 +0000</pubDate>
      <link>https://dev.to/dennervidal/monorepo-tooling-is-a-maintenance-decision-not-a-technical-one-2pm6</link>
      <guid>https://dev.to/dennervidal/monorepo-tooling-is-a-maintenance-decision-not-a-technical-one-2pm6</guid>
      <description>&lt;p&gt;Monorepo tooling isn't a technical decision. It's a maintenance decision.&lt;/p&gt;

&lt;p&gt;In 2024 I migrated our design system monorepo from Lerna to Lerna Lite. The diff was small in the packages and huge in the lockfile: 3,568 lines added, 11,647 removed. The reason: full Lerna was heavy, and all we used was publish automation and the changelog. Worse, it didn't work with pnpm's &lt;code&gt;workspace:&lt;/code&gt; protocol. Without it, internal dependencies were pinned to fixed versions in package.json (cortex-react pointed to cortex-core 0.3.1), and a package from our own repo got downloaded from npm again. With Lerna Lite everything became &lt;code&gt;workspace:*&lt;/code&gt;: local packages during development, and the real version written in at publish time. Along the way, yalc and the &lt;code&gt;lerna run&lt;/code&gt; scripts went away, replaced by &lt;code&gt;pnpm --filter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two years later, a different kind of debt: seven packages repeating the same dependency versions. I moved everything to the pnpm catalog. Today that's 23 dependencies declared in a single file, and every package.json points to &lt;code&gt;catalog:&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In the same commit I added an AGENTS.md at the root and one in every package (eight in total), explaining what the package does and how to run it. It helps new teammates and coding agents alike.&lt;/p&gt;

&lt;p&gt;As a tech lead I learned to change the question. Instead of "what's the best tool?", I ask: "who will understand this a year from now, without me?"&lt;/p&gt;

&lt;p&gt;In mentoring, that's the criterion I try to pass on: measure a tool by what it costs the next person.&lt;/p&gt;

&lt;p&gt;Which tooling decision would you make differently today?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>automation</category>
      <category>javascript</category>
      <category>tools</category>
    </item>
    <item>
      <title>Rollup to Rolldown without breaking the build contract</title>
      <dc:creator>Denner Vidal</dc:creator>
      <pubDate>Sat, 07 Nov 2026 22:28:34 +0000</pubDate>
      <link>https://dev.to/dennervidal/rollup-to-rolldown-without-breaking-the-build-contract-2fdi</link>
      <guid>https://dev.to/dennervidal/rollup-to-rolldown-without-breaking-the-build-contract-2fdi</guid>
      <description>&lt;p&gt;Swapping a bundler is easy. Knowing what you promised your consumers is the hard part.&lt;/p&gt;

&lt;p&gt;I migrated two packages of our design system (cortex-core and cortex-react) from Rollup to Rolldown. Before touching the tool, I wrote down the output contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESM with preserveModules in dist/esm&lt;/li&gt;
&lt;li&gt;types generated by tsc in dist/types&lt;/li&gt;
&lt;li&gt;default.css copied at the end of the build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of it stayed the same. Rolldown's config API is Rollup-compatible and the copy plugin's buildEnd hook still works. The two config files went from 42 and 45 lines down to 18 and 28.&lt;/p&gt;

&lt;p&gt;Two decisions worth recording: I dropped the CJS output, since both packages were already ESM-only, and I now clean dist before building so a stale artifact never gets published.&lt;/p&gt;

&lt;p&gt;The cost showed up 13 days later: the mobile Storybook stopped rendering with the new bundle's external packages, and the fix touched the Rolldown configs and added a browser shim to the React Native playground.&lt;/p&gt;

&lt;p&gt;Lesson: the contract I wrote covered whoever consumes the library, but not whoever consumes the bundle inside our own playground.&lt;/p&gt;

&lt;p&gt;When you change a build tool, which consumer do you forget to list?&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>tools</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Commit message as a mini-RFC</title>
      <dc:creator>Denner Vidal</dc:creator>
      <pubDate>Thu, 22 Oct 2026 20:04:04 +0000</pubDate>
      <link>https://dev.to/dennervidal/commit-message-as-a-mini-rfc-51i</link>
      <guid>https://dev.to/dennervidal/commit-message-as-a-mini-rfc-51i</guid>
      <description>&lt;p&gt;The best place to teach a junior engineer to think isn't a meeting. It's the commit message.&lt;/p&gt;

&lt;p&gt;A recent commit of mine has three parts, no template:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Problem: portal-based components render into the body and leak styles in Shadow DOM hosts.&lt;/li&gt;
&lt;li&gt;Approach: an optional, additive provider, no behavior change for anyone who doesn't use it.&lt;/li&gt;
&lt;li&gt;Out of scope: Uploader/Modal, since no consumer has asked yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That answers the three questions git blame will ask six months from now: why it changed, why this way, and why the rest didn't.&lt;/p&gt;

&lt;p&gt;For people early in their careers, the third part is the most valuable. Saying what you did NOT do shows scope was a decision, not an accident.&lt;/p&gt;

&lt;p&gt;In mentoring, I stop commenting on the code and start asking: "what's out of scope here?"&lt;/p&gt;

&lt;p&gt;Which review question has changed the way your team writes the most?&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>git</category>
      <category>learning</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>PortalProvider and Shadow DOM</title>
      <dc:creator>Denner Vidal</dc:creator>
      <pubDate>Tue, 20 Oct 2026 15:56:47 +0000</pubDate>
      <link>https://dev.to/dennervidal/portalprovider-and-shadow-dom-3dh5</link>
      <guid>https://dev.to/dennervidal/portalprovider-and-shadow-dom-3dh5</guid>
      <description>&lt;p&gt;Your popup escaped the Shadow DOM. And the obvious fix broke the host page.&lt;/p&gt;

&lt;p&gt;We ship a portal menu as a UMD web component embedded in JSF apps. FloatingPortal-based components (Tooltip, Select, Autocomplete, PhoneInput) render into document.body by default, which is outside the shadow root, so unstyled.&lt;/p&gt;

&lt;p&gt;The workaround was injecting a global stylesheet. Side effect: the whole Tailwind preflight leaked onto the host page and clobbered PrimeFaces and Bootstrap inputs.&lt;/p&gt;

&lt;p&gt;The fix is an optional PortalProvider. Each component takes a &lt;code&gt;root&lt;/code&gt; prop; if omitted it uses the nearest provider's root; with no provider it falls back to document.body. Nothing changes for anyone who doesn't opt in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useResolvedPortalRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;PortalRoot&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;portalRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePortalRoot&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// undefined = herdar do provider; null = opt-out explícito&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;portalRoot&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The detail that matters most: &lt;code&gt;root={null}&lt;/code&gt; has to mean "ignore the provider". With &lt;code&gt;root ?? portalRoot&lt;/code&gt;, null collapses into the fallback and the escape hatch is gone. So undefined and null behave differently on purpose.&lt;/p&gt;

&lt;p&gt;I left Uploader/Modal out on purpose. They still use createPortal, no consumer asked for it, and the fix pattern is different.&lt;/p&gt;

&lt;p&gt;Have you ever had to decide whether null means "I don't know" or "I don't want it" in a component API?&lt;/p&gt;

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