<?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: Ewan McDougall</title>
    <description>The latest articles on DEV Community by Ewan McDougall (@mrloop).</description>
    <link>https://dev.to/mrloop</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%2F92643%2F48095197-861d-49e8-a49e-875782f9c943.png</url>
      <title>DEV Community: Ewan McDougall</title>
      <link>https://dev.to/mrloop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrloop"/>
    <language>en</language>
    <item>
      <title>Scoped CSS in Ruby</title>
      <dc:creator>Ewan McDougall</dc:creator>
      <pubDate>Tue, 17 Jun 2025 06:05:00 +0000</pubDate>
      <link>https://dev.to/mrloop/scoped-css-in-ruby-4b65</link>
      <guid>https://dev.to/mrloop/scoped-css-in-ruby-4b65</guid>
      <description>&lt;h2&gt;
  
  
  The Component-Based CSS Challenge in Rails
&lt;/h2&gt;

&lt;p&gt;After years of working with JavaScript frameworks like Ember and React, I recently returned to Ruby on Rails for full-stack development. Modern JavaScript frameworks have spoiled us with elegant component-based CSS solutions, but what about Rails?&lt;/p&gt;

&lt;p&gt;When I started building UI components with the &lt;a href="https://github.com/viewcomponent/view_component" rel="noopener noreferrer"&gt;ViewComponent&lt;/a&gt; library, I was faced a familiar challenge: &lt;strong&gt;how do I scope CSS to individual components without leaks or conflicts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While the official ViewComponent docs &lt;a href="https://github.com/ViewComponent/view_component/blob/eba4c1424e58ceadc51d6f948c6ec8eb1b5a982e/docs/guide/javascript_and_css.md" rel="noopener noreferrer"&gt;previously offered some guidance&lt;/a&gt; on CSS scoping, these approaches were complex and have since been removed. I also found Julik Tarkhanov's interesting post on &lt;a href="https://blog.julik.nl/2025/04/template-scoped-css-in-rails" rel="noopener noreferrer"&gt;Template Scoped CSS in Rails&lt;/a&gt;, but it only scopes CSS to top-level templates - not granular enough for component-based development.&lt;/p&gt;

&lt;p&gt;I needed something that would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scope styles to individual ViewComponents&lt;/li&gt;
&lt;li&gt;Prevent CSS class name collisions&lt;/li&gt;
&lt;li&gt;Allow component reuse without duplicating styles&lt;/li&gt;
&lt;li&gt;Maintain the simplicity and developer experience Rails is known for&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this post, I'll walk through the solution I developed and eventually extracted into a gem called &lt;a href="https://github.com/mrloop/scoped_css" rel="noopener noreferrer"&gt;scoped_css&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We're Building: Component-Scoped CSS in Rails
&lt;/h2&gt;

&lt;p&gt;Ideally, we want to write component templates with encapsulated styles 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;style &lt;/span&gt;&lt;span class="na"&gt;scoped-to-element-below&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&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;"section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section Title&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when the component is rendered multiple times, we want the CSS to be included just once:&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;style &lt;/span&gt;&lt;span class="na"&gt;scoped-to-elements-below&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&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;"section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section Title&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&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;"section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Another Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for this to work properly in Rails, we need a mechanism to transform this into uniquely prefixed CSS selectors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;scoped_css&lt;/code&gt; Helper: How It Works
&lt;/h2&gt;

&lt;p&gt;Here's the helper function that makes component-scoped CSS possible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;scoped_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;css_block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@per_template_outputs&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

  &lt;span class="c1"&gt;# Capture the CSS content from the block&lt;/span&gt;
  &lt;span class="n"&gt;css_block_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;block_given?&lt;/span&gt;
    &lt;span class="n"&gt;css_block_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;css_block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Generate a unique prefix based on the CSS content&lt;/span&gt;
  &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"a&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;Digest&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SHA1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css_block_content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
  &lt;span class="n"&gt;prefixed_css_block_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;local?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;' &amp;lt;!-- previously output --&amp;gt; '&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;

  &lt;span class="c1"&gt;# Check if we've already processed this CSS block&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@per_template_outputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has_key?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Reuse the existing styles mapping but don't output the CSS again&lt;/span&gt;
    &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@per_template_outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c1"&gt;# Process the CSS block, prefix all class selectors&lt;/span&gt;
    &lt;span class="n"&gt;prefixed_css_block_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix_css_classes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;css_block_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Cache the styles mapping for future renders of this component&lt;/span&gt;
    &lt;span class="vi"&gt;@per_template_outputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prefixed_css_block_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respond_to?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:html_safe&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;prefixed_css_block_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prefixed_css_block_content&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lets walk through what's happening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Content Capture&lt;/strong&gt;: We grab the CSS content from the provided block.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prefix Generation&lt;/strong&gt;: We create a unique prefix by hashing the CSS content with SHA1. Why SHA1? It gives us a consistent, unique identifier for identical CSS blocks, allowing us to detect duplicates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CSS Caching&lt;/strong&gt;: The &lt;code&gt;@per_template_outputs&lt;/code&gt; hash serves as our CSS cache. We check if we've already processed this CSS block (based on its prefix) to avoid duplicating styles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prefixing CSS Classes&lt;/strong&gt;: For new CSS blocks, we transform each CSS class by adding our unique prefix (handled in the &lt;code&gt;prefix_css_classes&lt;/code&gt; method, not shown here).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: We return three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The prefixed CSS (or empty string if already rendered)&lt;/li&gt;
&lt;li&gt;A mapping of original class names to prefixed ones&lt;/li&gt;
&lt;li&gt;The unique prefix for this CSS block&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is CSS that 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;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.agkd94j4-section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.agkd94j4-heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&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;"agkd94j4-section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"agkd94j4-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section Title&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&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;"agkd94j4-section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"agkd94j4-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Another Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our CSS is effectively scoped to the component while ensuring styles are only included once, no matter how many times we render the component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together: Complete Example
&lt;/h2&gt;

&lt;p&gt;Let's see how this works in a real Rails application with ViewComponents:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In your main template (app/views/home/index.html.erb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scoped_css&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:heading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;SectionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  Section 1
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;SectionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  Section 2
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In your component class (app/components/section_component.rb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SectionComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In your component template (app/components/section_component.html.erb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scoped_css&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&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;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:section&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:heading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates HTML with properly scoped CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/home/index.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"atge5q2e-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- app/components/section_component.html.erb --&amp;gt;&lt;/span&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;"agkd94j4-section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"agkd94j4-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  Section 1
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&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;"agkd94j4-section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"agkd94j4-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  Section 2
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.agkd94j4-section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.agkd94j4-heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- app/views/home/index.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.atge5q2e-heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Usage: Attribute Splatting
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to apply HTML attributes to a component from the parent template. Let's enhance our solution to handle this common pattern:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In your main template (app/views/home/index.html.erb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scoped_css&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:heading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;SectionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;attributes: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"important-section"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:section&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Section 1&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In your component class (app/components/section_component.rb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SectionComponent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ViewComponent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;attributes: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="vi"&gt;@attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attributes&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In your component template (app/components/section_component.html.erb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scoped_css&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="py"&gt;scroll-snap-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;purple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;section&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;helpers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splat_attributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@attributes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:section&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:heading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;splat_attributes&lt;/code&gt; helper combines the component's internal class with any external classes passed from the parent:&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;"agkd94j4-section atge5q2e-section"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"important-section"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"agkd94j4-heading"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Section&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Section 1&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/section&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Rather than using something like &lt;code&gt;&amp;lt;section class="&amp;lt;%= styles[:heading] %&amp;gt;" &amp;lt;%= helpers.splat_attributes(@attributes, styles[:section]) %&amp;gt;&amp;gt;&lt;/code&gt;, we use the &lt;code&gt;splat_attributes&lt;/code&gt; helper to handle both the component's internal classes and any additional attributes passed from the parent.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  CSS Specificity and Customization
&lt;/h2&gt;

&lt;p&gt;What if you want to customize the appearance of a specific component instance? The CSS cascade works in our favor here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In your main template (app/views/home/index.html.erb):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scoped_css&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;darkgreen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c"&gt;/* This will override the component's purple color */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.heading&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:heading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Title&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;SectionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;attributes: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="n"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:section&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Section 1 (Dark Green)&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="no"&gt;SectionComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Section 2 (Purple)&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;style_string&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we render style tags at the bottom of each template, CSS specificity works as expected:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The component's internal styles are rendered first&lt;/li&gt;
&lt;li&gt;The parent template styles are rendered later&lt;/li&gt;
&lt;li&gt;The last declared selector with the same specificity takes precedence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the first section with the parent's class applied, the color will be dark green (overriding the component's purple). The second section, without the parent's class, will remain purple.&lt;/p&gt;

&lt;p&gt;This scoped CSS solution offers several key benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;True Encapsulation&lt;/strong&gt;: Component styles don't leak or conflict, even with identical class names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: CSS is only included once per unique component, not per instance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainability&lt;/strong&gt;: Components remain self-contained with their styles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customization&lt;/strong&gt;: The CSS cascade still works as expected for intentional overrides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;: No additional build steps or preprocessors required&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion: Component-Based CSS in Rails
&lt;/h2&gt;

&lt;p&gt;By implementing this scoped CSS solution, we've bridged the gap between modern component-based frontend frameworks and Rails ViewComponents. We get the best of both worlds: Rails' simplicity and productivity with the style encapsulation we've come to expect from JavaScript frameworks.&lt;/p&gt;

&lt;p&gt;For a drop-in solution, check out the &lt;a href="https://github.com/mrloop/scoped_css" rel="noopener noreferrer"&gt;scoped_css gem&lt;/a&gt; I've extracted from this approach. It includes the helpers demonstrated.&lt;/p&gt;

</description>
      <category>css</category>
      <category>rails</category>
      <category>frontend</category>
      <category>rubygems</category>
    </item>
    <item>
      <title>Exploring LLMs: A Blind Trial for Code Completions</title>
      <dc:creator>Ewan McDougall</dc:creator>
      <pubDate>Sun, 09 Mar 2025 18:20:00 +0000</pubDate>
      <link>https://dev.to/mrloop/exploring-llms-a-blind-trial-for-code-completions-3l70</link>
      <guid>https://dev.to/mrloop/exploring-llms-a-blind-trial-for-code-completions-3l70</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Abstract:&lt;/strong&gt; This article presents findings from a four-month blind trial of LLM code completions in a professional development environment. By tracking over 94,000 code suggestions across three leading AI assistants, I found that GitHub Copilot delivers the highest quality suggestions (3.4% acceptance rate) while Supermaven provides the greatest volume of useful completions. Codeium performed significantly worse (0.5% acceptance rate) and was discontinued after seven days. This data-driven approach reveals meaningful differences in LLM performance for practical coding assistance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Large language models (LLMs) have transformed the coding experience with capabilities ranging from auto-completion to chat interfaces and agentic file editing. In this post, I'll share results from a systematic evaluation of three popular AI code completion tools in a professional development environment.&lt;/p&gt;

&lt;p&gt;Rather than relying on subjective impressions, I conducted a rigorous blind trial to determine which model genuinely performs best in day-to-day coding work. This methodology eliminates the confirmation bias that often affects tool selection ("I think this one feels better") and provides concrete data about which assistant actually delivers the most value.&lt;/p&gt;

&lt;p&gt;To conduct a randomized blind trial of large language model code completions, I established four key requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure multiple LLMs for code completions in my development environment&lt;/li&gt;
&lt;li&gt;Display completion suggestions without revealing which LLM generated them&lt;/li&gt;
&lt;li&gt;Randomize the presentation order to eliminate selection bias&lt;/li&gt;
&lt;li&gt;Collect comprehensive usage data over an extended period&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  LLMs Under Evaluation
&lt;/h3&gt;

&lt;p&gt;I selected three popular AI code assistants for this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot&lt;/strong&gt;: Microsoft and GitHub's AI pair programmer, built on OpenAI technology, trained on public code repositories and avialbe as a paid subscription service ($10/month or $100/year).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Codeium&lt;/strong&gt;: A free AI coding assistant from Exafunction that promises high-quality completions with low latency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supermaven&lt;/strong&gt;: AI code completion tool that claims to offer deeper contextual understanding and multi-line completions. Available as both free and paid tiers. The free tier was used for this trial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By comparing these different systems, I aimed to determine which would be most effective for my daily development workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;In this section, I'll explain the technical approach used to create a fair testing environment for these AI auto completions. If you're primarily interested in the findings rather than the methodology, feel free to skip directly to the results section.&lt;/p&gt;

&lt;p&gt;I use Neovim daily; it is highly customizable and can be configured to meet my needs. The configuration samples shown use &lt;a href="https://github.com/folke/lazy.nvim" rel="noopener noreferrer"&gt;Lazy.nvim&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLM Configurations
&lt;/h3&gt;

&lt;p&gt;Codeium, GitHub Copilot, and Supermaven are used for code completions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"Exafunction/codeium.nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"CODEIUM"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"nvim-lua/plenary.nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"hrsh7th/nvim-cmp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&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="s2"&gt;"zbirenbaum/copilot.lua"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"COPILOT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Copilot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"InsertEnter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;suggestion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;panel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="s2"&gt;"zbirenbaum/copilot-cmp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"COPILOT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"copilot.lua"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"lspkind.nvim"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_set_hl&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="s2"&gt;"CmpItemKindCopilot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"#6CC644"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&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="s2"&gt;"supermaven-inc/supermaven-nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;os.getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SUPERMAVEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;disable_inline_completion&lt;/span&gt; &lt;span class="o"&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="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_set_hl&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="s2"&gt;"CmpItemKindSupermaven"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;fg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"#6CC644"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  UI Configuration
&lt;/h3&gt;

&lt;p&gt;For each LLM source, only a symbol is shown in the UI, and the same symbol is used for all LLM completions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;-- only showing formatting option for brevity&lt;/span&gt;
  &lt;span class="n"&gt;formatting&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'menu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'abbr'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'kind'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'lspkind'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;cmp_format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"symbol"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;maxwidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;show_labelDetails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;ellipsis_char&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;symbol_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Supermaven&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Codeium&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Copilot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="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;h3&gt;
  
  
  Randomize the Order of Completion Suggestions
&lt;/h3&gt;

&lt;p&gt;To eliminate position bias (the tendency to select the first suggestion), I implemented a system to randomize the order in which completion suggestions appear. Rather than defining a fixed order of completion sources in the setup function, I dynamically shuffle them each time a new buffer is loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; cmp.setup({
  -- only showing sources option for brevity
&lt;span class="gd"&gt;-  sources = cmp.config.sources({
-    { name = 'nvim_lsp' },
-    { name = 'luasnip' },
-    { name = 'path' },
-  }, {
-    { name = 'nvim-cmp-ts-tag-close' },
-    { name = "codeium" },
-    { name = 'copilot'},
-    { name = 'supermaven'},
-    { name = 'nvim_lua' },
-    { name = 'spell' },
-  }, {
-    { name = 'buffer' },
-  }),
-
&lt;/span&gt; })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach ensures that no AI completion source consistently appears first in the suggestion list, which is crucial for unbiased evaluation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Fisher-Yates shuffle algorithm to randomize suggestion order&lt;/span&gt;
&lt;span class="c1"&gt;-- This is critical for reducing selection bias - if suggestions always appeared&lt;/span&gt;
&lt;span class="c1"&gt;-- in the same order, I might unconsciously favor the first option&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;math.random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_create_autocmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'BufReadPre'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'nvim_lsp'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'luasnip'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'path'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;shuffle&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'nvim-cmp-ts-tag-close'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"codeium"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'copilot'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'supermaven'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'nvim_lua'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'spell'&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="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'buffer'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sources&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fvko50wjcqvslbeweoraf.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%2Fvko50wjcqvslbeweoraf.png" alt="Auto completion UI" width="800" height="296"&gt;&lt;/a&gt;&lt;br&gt;
Fig 1: Example of the auto completion UI showing showing suggestions for &lt;code&gt;this.objectUrl&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Data Collection System
&lt;/h3&gt;

&lt;p&gt;To objectively measure each LLM's performance, I built a comprehensive data collection system using SQLite. This system silently recorded every completion suggestion and user acceptance decision during my normal development activities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Setup lsqlite3&lt;/span&gt;
&lt;span class="c1"&gt;-- Run `luarocks install lsqlite3` if needed&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;pcall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'lsqlite3'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;db_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'data'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s1"&gt;'/completion-log.sqlite3'&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;-- Create a log table if it doesn't exist&lt;/span&gt;
  &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="s"&gt;[[
    CREATE TABLE IF NOT EXISTS cmp_log (
      id INTEGER PRIMARY KEY,
      timestamp TEXT,
      event TEXT,
      multiline INTEGER,
      source_name TEXT,
      filetype TEXT
    )
  ]]&lt;/span&gt;

  &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Logging completions to "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;-- Prepare the insert statement&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"INSERT INTO cmp_log (timestamp, event, multiline, source_name, filetype) VALUES (?, ?, ?, ?, ?)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;-- Close the database connection when Neovim exits&lt;/span&gt;
  &lt;span class="n"&gt;vim&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nvim_create_autocmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"VimLeavePre"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;finalize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;-- Table of source names to log&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;log_sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;supermaven&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;codeium&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;copilot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cmp_ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;-- Function to log completion events&lt;/span&gt;
  &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;log_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filetype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;multiline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textEdit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textEdit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newText&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;textEdit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;newText&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;log_sources&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
      &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;bind_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;os.date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'%Y-%m-%d %H:%M:%S'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filetype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;


  &lt;span class="c1"&gt;-- Register event listeners&lt;/span&gt;
  &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'menu_opened'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;ipairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;source_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
      &lt;span class="n"&gt;log_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"completion_suggested"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;get_completion_item&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;source_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filetype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;cmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'confirm_done'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;log_completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"completion_used"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;get_completion_item&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filetype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://www.sqlite.org/whentouse.html" rel="noopener noreferrer"&gt;SQLite is used&lt;/a&gt; because it's lightweight, requires no server setup, and provides a self-contained database solution ideal for this type of data collection. Additionally, &lt;a href="https://datasette.io/" rel="noopener noreferrer"&gt;Datasette&lt;/a&gt; can be used to easily query, visualize, and publish the data for later analysis.&lt;/p&gt;

&lt;p&gt;The database schema tracked five key data points for each completion event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;cmp_log&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;multiline&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;source_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;filetype&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;timestamp&lt;/code&gt; is the date and time the event was logged and is useful to see if the effectiveness of the LLMs changes over time. The &lt;code&gt;event&lt;/code&gt; is the name of the &lt;code&gt;nvim-cmp&lt;/code&gt; event that was fired. The &lt;code&gt;multiline&lt;/code&gt; is a boolean value that is true if the completion is multiline. The &lt;code&gt;source_name&lt;/code&gt; is the name of the source that provided the completion, for example, &lt;code&gt;Copilot&lt;/code&gt;. The &lt;code&gt;filetype&lt;/code&gt; is the filetype of the buffer that the completion was provided in.&lt;/p&gt;

&lt;p&gt;The system tracked two primary event types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;completion_suggested&lt;/strong&gt;: Recorded whenever a completion was offered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;completion_used&lt;/strong&gt;: Recorded when I accepted a completion&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach allowed me to calculate the critical acceptance rate metric (accepted completions ÷ suggested completions) that forms the foundation of this analysis.&lt;/p&gt;

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

&lt;p&gt;After collecting data for approximately four months across real-world development projects, clear patterns emerged in how these AI assistants performed in day-to-day coding scenarios.&lt;/p&gt;

&lt;p&gt;The most immediate finding was Codeium's poor performance. It achieved only a 0.5% acceptance rate (meaning I accepted only 1 out of every 200 suggestions). This led me to discontinue it after seven days of evaluation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quantitative Results
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Summary Statistics&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LLM&lt;/th&gt;
&lt;th&gt;Total Suggestions&lt;/th&gt;
&lt;th&gt;Accepted Completions&lt;/th&gt;
&lt;th&gt;Acceptance Rate&lt;/th&gt;
&lt;th&gt;Avg Daily Suggestions&lt;/th&gt;
&lt;th&gt;Avg Daily Acceptances&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supermaven&lt;/td&gt;
&lt;td&gt;60,508&lt;/td&gt;
&lt;td&gt;1,171&lt;/td&gt;
&lt;td&gt;1.9%&lt;/td&gt;
&lt;td&gt;293.3&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Copilot&lt;/td&gt;
&lt;td&gt;27,395&lt;/td&gt;
&lt;td&gt;921&lt;/td&gt;
&lt;td&gt;3.4%&lt;/td&gt;
&lt;td&gt;133.5&lt;/td&gt;
&lt;td&gt;4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codeium&lt;/td&gt;
&lt;td&gt;7, 091&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;0.5%&lt;/td&gt;
&lt;td&gt;745.4&lt;/td&gt;
&lt;td&gt;3.4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://completion-log-281060383488.europe-west1.run.app/completion-log?sql=WITH+daily_stats+AS+%28%0D%0A++SELECT+%0D%0A++++source_name+AS+LLM%2C%0D%0A++++COUNT%28*%29+AS+total_suggestions%2C%0D%0A++++SUM%28CASE+WHEN+event+%3D+%27completion_used%27+THEN+1+ELSE+0+END%29+AS+accepted_completions%2C%0D%0A++++%28JULIANDAY%28MAX%28timestamp%29%29+-+JULIANDAY%28MIN%28timestamp%29%29+%2B+1%29+AS+total_days%0D%0A++FROM+cmp_log%0D%0A++WHERE+event+IN+%28%27completion_suggested%27%2C+%27completion_used%27%29%0D%0A++GROUP+BY+source_name%0D%0A%29%0D%0ASELECT+%0D%0A++LLM%2C%0D%0A++total_suggestions+AS+%22Total+Suggestions%22%2C%0D%0A++accepted_completions+AS+%22Accepted+Completions%22%2C%0D%0A++ROUND%28%28accepted_completions+*+100.0%29+%2F+total_suggestions%2C+1%29+%7C%7C+%27%25%27+AS+%22Acceptance+Rate%22%2C%0D%0A++ROUND%28total_suggestions+%2F+total_days%2C+1%29+AS+%22Avg+Daily+Suggestions%22%2C%0D%0A++ROUND%28accepted_completions+%2F+total_days%2C+1%29+AS+%22Avg+Daily+Acceptances%22%2C%0D%0A++total_days%0D%0AFROM+daily_stats%0D%0AORDER+BY+total_suggestions+DESC%3B%0D%0A" rel="noopener noreferrer"&gt;Data here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These statistics reveal two distinct AI assistant strategies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copilot&lt;/strong&gt;: Offers fewer but higher quality suggestions (3.4% acceptance)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supermaven&lt;/strong&gt;: Provides more aggressive suggestions with lower precision (1.9% acceptance)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Daily Used Count&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://blog.mrloop.com/images/code-completion/daily-used-count.svg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.mrloop.com%2Fimages%2Fcode-completion%2Fdaily-used-count.svg" alt="daily used count chart" width="2254" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 1: Daily count of accepted completions by LLM. Note Supermaven's consistently higher daily usage (average: 5.7 completions) compared to Copilot (average: 4.5 completions).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily Suggested Count&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://blog.mrloop.com/images/code-completion/daily-suggested-count.svg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.mrloop.com%2Fimages%2Fcode-completion%2Fdaily-suggested-count.svg" alt="daily suggested count chart" width="2254" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 2: Daily count of suggested completions by LLM. Supermaven offers significantly more suggestions (an average of 293 per day) than Copilot (134 per day).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily Acceptance Ratio&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://blog.mrloop.com//images/code-completion/daily-used-over-suggested-ratio.svg" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblog.mrloop.com%2Fimages%2Fcode-completion%2Fdaily-used-over-suggested-ratio.svg" alt="daily used over suggested ratio chart" width="2254" height="298"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig 3: Daily acceptance ratio (accepted/suggested) by LLM. Copilot consistently maintains higher suggested to accepted completion ratios (3.4%), while Supermaven typically ranges between 0.1.5-0.2% of suggestions accepted.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://completion-log-281060383488.europe-west1.run.app/completion-log?sql=WITH+daily_counts+AS+%28%0D%0A++++SELECT+%0D%0A++++++++DATE%28timestamp%29+AS+day%2C+%0D%0A++++++++source_name%2C+%0D%0A++++++++COUNT%28CASE+WHEN+event+%3D+%27completion_used%27+THEN+1+END%29+AS+daily_used_count%2C%0D%0A++++++++COUNT%28CASE+WHEN+event+%3D+%27completion_suggested%27+THEN+1+END%29+AS+daily_suggested_count%0D%0A++++FROM+cmp_log%0D%0A++++WHERE+source_name+%21%3D+%27cmp_ai%27%0D%0A++++GROUP+BY+day%2C+source_name%0D%0A%29%0D%0ASELECT+%0D%0A++++day%2C%0D%0A++++source_name%2C%0D%0A++++daily_used_count%2C%0D%0A++++daily_suggested_count%2C%0D%0A++++ROUND%28%0D%0A++++++++CAST%28daily_used_count+AS+FLOAT%29+%2F+NULLIF%28daily_suggested_count%2C+0%29%2C+%0D%0A++++++++5%0D%0A++++%29+AS+daily_used_suggested_ratio%2C%0D%0A++++SUM%28daily_used_count%29+OVER+%28%0D%0A++++++++PARTITION+BY+source_name+%0D%0A++++++++ORDER+BY+day+%0D%0A++++++++ROWS+BETWEEN+UNBOUNDED+PRECEDING+AND+CURRENT+ROW%0D%0A++++%29+AS+cumulative_used_count%2C%0D%0A++++SUM%28daily_suggested_count%29+OVER+%28%0D%0A++++++++PARTITION+BY+source_name+%0D%0A++++++++ORDER+BY+day+%0D%0A++++++++ROWS+BETWEEN+UNBOUNDED+PRECEDING+AND+CURRENT+ROW%0D%0A++++%29+AS+cumulative_suggested_count%2C%0D%0A++++ROUND%28%0D%0A++++++++CAST%28%0D%0A++++++++++++SUM%28daily_used_count%29+OVER+%28%0D%0A++++++++++++++++PARTITION+BY+source_name+%0D%0A++++++++++++++++ORDER+BY+day+%0D%0A++++++++++++++++ROWS+BETWEEN+UNBOUNDED+PRECEDING+AND+CURRENT+ROW%0D%0A++++++++++++%29+AS+FLOAT%0D%0A++++++++%29+%2F+NULLIF%28%0D%0A++++++++++++SUM%28daily_suggested_count%29+OVER+%28%0D%0A++++++++++++++++PARTITION+BY+source_name+%0D%0A++++++++++++++++ORDER+BY+day+%0D%0A++++++++++++++++ROWS+BETWEEN+UNBOUNDED+PRECEDING+AND+CURRENT+ROW%0D%0A++++++++++++%29%2C+0%0D%0A++++++++%29%2C+%0D%0A++++++++5%0D%0A++++%29+AS+cumulative_used_suggested_ratio%0D%0AFROM+daily_counts%0D%0AORDER+BY+day%2C+source_name%3B%0D%0A#g.mark=bar&amp;amp;g.x_column=day&amp;amp;g.x_type=ordinal&amp;amp;g.y_column=daily_suggested_count&amp;amp;g.y_type=quantitative&amp;amp;g.color_column=source_name" rel="noopener noreferrer"&gt;View raw data here&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance by Language
&lt;/h3&gt;

&lt;p&gt;Analysis of completions by file type revealed interesting patterns:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Most Accurate LLM&lt;/th&gt;
&lt;th&gt;Acceptance Rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;zsh&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;5.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;typescriptreact&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;5.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;css&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;5.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;handlebars&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jsonc&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lua&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;4.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;make&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;3.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;javascript.glimmer&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;3.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript/TypeScript&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;conf&lt;/td&gt;
&lt;td&gt;copilot&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;yaml&lt;/td&gt;
&lt;td&gt;supermaven&lt;/td&gt;
&lt;td&gt;1.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;git*&lt;/td&gt;
&lt;td&gt;supermaven&lt;/td&gt;
&lt;td&gt;1.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;markdown&lt;/td&gt;
&lt;td&gt;supermaven&lt;/td&gt;
&lt;td&gt;0.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://completion-log-281060383488.europe-west1.run.app/completion-log?sql=WITH+llm_stats+AS+%28%0D%0A++SELECT+%0D%0A++++CASE+%0D%0A++++++WHEN+filetype+IN+%28%27javascript%27%2C+%27typescript%27%2C+%27javascriptreact%27%29+THEN+%27JavaScript%2FTypeScript%27%0D%0A++++++WHEN+filetype+LIKE+%27git%25%27+THEN+%27Git%27%0D%0A++++++ELSE+filetype%0D%0A++++END+AS+Language%2C%0D%0A++++source_name+AS+LLM%2C%0D%0A++++COUNT%28*%29+AS+total_suggestions%2C%0D%0A++++SUM%28CASE+WHEN+event+%3D+%27completion_used%27+THEN+1+ELSE+0+END%29+AS+accepted_completions%0D%0A++FROM+cmp_log%0D%0A++WHERE+event+IN+%28%27completion_suggested%27%2C+%27completion_used%27%29%0D%0A++AND+source_name+%21%3D+%27cmp_ai%27++--+Exclude+cmp_ai%0D%0A++GROUP+BY+Language%2C+source_name%0D%0A%29%2C%0D%0Aranked_llms+AS+%28%0D%0A++SELECT+%0D%0A++++Language%2C%0D%0A++++LLM+AS+%22Most+Accurate+LLM%22%2C%0D%0A++++ROUND%28%28accepted_completions+*+100.0%29+%2F+total_suggestions%2C+1%29+AS+acceptance_rate%2C%0D%0A++++RANK%28%29+OVER+%28PARTITION+BY+Language+ORDER+BY+%28accepted_completions+*+100.0%29+%2F+total_suggestions+DESC%29+AS+rnk%0D%0A++FROM+llm_stats%0D%0A%29%0D%0ASELECT+Language%2C+%22Most+Accurate+LLM%22%2C+acceptance_rate+%7C%7C+%27%25%27+AS+%22Acceptance+Rate%22%0D%0AFROM+ranked_llms%0D%0AWHERE+rnk+%3D+1+AND+acceptance_rate+%3E+0++--+Ignore+0.0%25+acceptance+rates%0D%0AORDER+BY+acceptance_rate+DESC%3B%0D%0A" rel="noopener noreferrer"&gt;Data here&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Out of Codeium, Copilot, and Supermaven, Codeium is the clear loser the lowest acceptance rate (0.5%).&lt;/p&gt;

&lt;p&gt;Supermaven provided the highest volume of useful completions (1,171 accepted suggestions) but at the cost of efficiency - it generated 60508 total suggestions, meaning 98.1% of its completions were rejected.&lt;/p&gt;

&lt;p&gt;Copilot demonstrated the best balance of quality and quantity - with 921 accepted completions from just 27395 suggestions, achieving a 3.4% acceptance rate. More than twice the acceptance rate of Supermaven.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations and Future Work
&lt;/h3&gt;

&lt;p&gt;While this study provides valuable insights, several limitations should be acknowledged:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single-user perspective&lt;/strong&gt; - Results primarily reflect my personal coding style, projects, and preferences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project-specific patterns&lt;/strong&gt; - Performance likely varies across different codebases and domains&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Temporal factors&lt;/strong&gt; - AI models are continuously improving; results may not reflect current capabilities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial focus&lt;/strong&gt; - Only commercial LLMs were tested; promising open-source models weren't evaluated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data collection gaps&lt;/strong&gt; - There were two notable gaps in data collection:

&lt;ul&gt;
&lt;li&gt;Eight days in July with no Copilot data (likely due to authentication expiration)&lt;/li&gt;
&lt;li&gt;A period between October and December when I transitioned to a new development machine&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Based on these findings and limitations, I've identified several opportunities for future work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expanded model comparison&lt;/strong&gt;: Integrate additional code completion tools, particularly open-source models via plugins like &lt;a href="https://github.com/tzachar/cmp-ai" rel="noopener noreferrer"&gt;cmp-ai&lt;/a&gt; or &lt;a href="https://github.com/milanglacier/minuet-ai.nvim" rel="noopener noreferrer"&gt;minuet-ai.nvim&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Methodological refinements&lt;/strong&gt;: Switch from the &lt;code&gt;menu_opened&lt;/code&gt; event to &lt;code&gt;menu_closed&lt;/code&gt; for asynchronous completions to ensure all displayed suggestions are properly captured. This would provide a more accurate picture of what completions were actually seen and evaluated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Multi-user study&lt;/strong&gt;: Expand data collection to include other developers to better understand how these tools perform across different coding styles and domains.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This blind trial of LLM code completions revealed significant differences between the three tested models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codeium&lt;/strong&gt;: Discontinued after 7 days due to poor performance (0.5% acceptance rate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot&lt;/strong&gt;: Highest quality suggestions (3.4% acceptance rate)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supermaven&lt;/strong&gt;: Highest volume of accepted suggestions but lower efficiency (1.9% acceptance rate)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The findings demonstrate that different LLMs have distinct strengths - Copilot excels at providing precise, focused suggestions that are more likely to be accepted, while Supermaven offers more aggressive suggestions covering a wider range of possibilities.&lt;/p&gt;

&lt;p&gt;The quantitative approach revealed patterns that wouldn't be obvious from casual usage, such as the significant difference in acceptance rates between the tools (Copilot's 3.4% vs. Supermaven's 1.9%).&lt;/p&gt;

&lt;p&gt;Based on these results, I've adopted a hybrid approach in my development workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Continue using Copilot for auto complete.&lt;/li&gt;
&lt;li&gt;Trial paid for Supermaven to see if this improves the acceptance rate.&lt;/li&gt;
&lt;li&gt;Test new models to ensure I'm using the most effective tools&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This experiment demonstrates the value of data-driven decision making when selecting developer tools, and I look forward to expanding this methodology to evaluate additional LLM-based coding assistants in the future.&lt;/p&gt;

</description>
      <category>neovim</category>
      <category>llm</category>
      <category>ai</category>
    </item>
    <item>
      <title>useSuspenseQuery</title>
      <dc:creator>Ewan McDougall</dc:creator>
      <pubDate>Sun, 16 Jun 2024 13:50:00 +0000</pubDate>
      <link>https://dev.to/mrloop/usesuspensequery-eln</link>
      <guid>https://dev.to/mrloop/usesuspensequery-eln</guid>
      <description>&lt;p&gt;I was interested in how nested React &lt;a href="https://react.dev/reference/react/Suspense"&gt;&lt;code&gt;&amp;lt;Suspense/&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://tanstack.com/query/"&gt;TanStack useSuspenseQuery&lt;/a&gt; worked together. The small app below is a visualization of various combinations of &lt;a href="https://react.dev/reference/react/Suspense"&gt;&lt;code&gt;&amp;lt;Suspense/&amp;gt;&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://tanstack.com/query/"&gt;TanStack Query&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe src="https://stackblitz.com/edit/mrloop-use-suspense-query?embed=1&amp;amp;file=src%2FApp.tsx" width="100%" height="500"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The source code is available at &lt;a href="https://github.com/mrloop/use-suspense-query"&gt;GitHub&lt;/a&gt;. Using the custom &lt;code&gt;&amp;lt;DelayedQuery/&amp;gt;&lt;/code&gt; component it is easy to compose different loading scenarios to see how they behave. Give it a try, edit and run the code at &lt;a href="https://stackblitz.com/~/github.com/mrloop/use-suspense-query"&gt;StackBlitz&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Nested&lt;/strong&gt; shows 3 nested &lt;code&gt;&amp;lt;Suspense/&amp;gt;&lt;/code&gt;, and within each &lt;code&gt;&amp;lt;Suspense/&amp;gt;&lt;/code&gt; a &lt;code&gt;useSuspenseQuery&lt;/code&gt; is run. The fetches run in serial. The loading state for the first query is shown, then when that is loaded the loading state for the second query and then when that is loaded the loading state for the third query is shown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel&lt;/strong&gt; shows a &lt;code&gt;&amp;lt;Suspense/&amp;gt;&lt;/code&gt; containing 3 &lt;code&gt;useSuspenseQuery&lt;/code&gt;, with 2 fetches taking 1 second and 1 fetch taking 2 seconds. A single loading state is shown until the last query finishes after 2 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cached&lt;/strong&gt; shows two variations. The first variation &lt;strong&gt;Fast load first&lt;/strong&gt; show a &lt;code&gt;&amp;lt;Suspense/&amp;gt;&lt;/code&gt; containing 3 &lt;code&gt;useSuspenseQuery&lt;/code&gt; as in the &lt;strong&gt;Parallel&lt;/strong&gt; example but in this case all the queries use the same cache key. They are making the same request and can make use of the cache. The first request will take 4 seconds, the second will take 10 seconds and the third will take 10 seconds. A single loading state is shown until the first 4 second request completes, the other queries don't attempt to make a query but instead wait for the first query to run that has the same cache key and use the cached result. All results are displayed after 4 seconds.&lt;/p&gt;

&lt;p&gt;The second variation &lt;strong&gt;Slow load first&lt;/strong&gt; is the same as &lt;strong&gt;Fast load first&lt;/strong&gt; but the order of the queries is changed, in this case the first request will take 10 seconds, the second request will take 4 seconds and the third request will take 10 seconds. A single loading state is shown until the first 10 second request completes, the other queries don't attempt to make a query but instead wait for the first query to run that has the same cache key and use the cached result. All results are displayed after 10 seconds.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>tanstack</category>
    </item>
    <item>
      <title>Quick full-stack app deployment using AWS and Ember.js</title>
      <dc:creator>Ewan McDougall</dc:creator>
      <pubDate>Mon, 01 May 2023 13:38:00 +0000</pubDate>
      <link>https://dev.to/mrloop/quick-full-stack-app-deployment-using-aws-and-emberjs-1gl0</link>
      <guid>https://dev.to/mrloop/quick-full-stack-app-deployment-using-aws-and-emberjs-1gl0</guid>
      <description>&lt;p&gt;I've been working on a small side project, more on that later, and wanted a straight forward way to deploy a serverless full-stack app. I'm using &lt;a href="https://emberjs.com/"&gt;Ember.js&lt;/a&gt; as the front-end framework and the back-end is &lt;a href="https://aws.amazon.com/"&gt;AWS&lt;/a&gt;, Amazon Web Services, &lt;a href="https://aws.amazon.com/lambda/"&gt;Lambda&lt;/a&gt; and other AWS services. This post describes the minimal setup to deploy a full-stack app with &lt;a href="https://emberjs.com/"&gt;Ember.js&lt;/a&gt; and a &lt;a href="https://aws.amazon.com/lambda/"&gt;Lambda&lt;/a&gt; API endpoint.&lt;/p&gt;

&lt;p&gt;We're going to create a simple hello world &lt;a href="https://emberjs.com/"&gt;Ember.js&lt;/a&gt; app that is deployed to AWS with Serverless Stack, &lt;a href="https://sst.dev/"&gt;SST&lt;/a&gt;, and uses a &lt;code&gt;hello&lt;/code&gt; API endpoint. There are lots of good docs on SST at &lt;a href="https://docs.sst.dev"&gt;https://docs.sst.dev&lt;/a&gt;. We'll use the &lt;a href="https://docs.sst.dev/start/standalone"&gt;'Get Started'&lt;/a&gt; instructions to create the SST project and then add our &lt;a href="https://emberjs.com/"&gt;Ember.js&lt;/a&gt; app to it.&lt;/p&gt;

&lt;p&gt;You'll need an AWS account and &lt;a href="https://docs.sst.dev/advanced/iam-credentials#loading-from-a-file"&gt;AWS credentials configured locally&lt;/a&gt;. We'll use &lt;a href="https://pnpm.io/"&gt;pnpm&lt;/a&gt; but you could also use &lt;a href="https://docs.npmjs.com/cli"&gt;npm&lt;/a&gt; or &lt;a href="https://yarnpkg.com/getting-started"&gt;yarn&lt;/a&gt;. The finished app is available &lt;a href="https://github.com/mrloop/ember-sst-example"&gt;on github&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a new app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm create sst ember-sst-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start your &lt;a href="https://docs.sst.dev/live-lambda-development"&gt;local dev environment&lt;/a&gt;.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm sst dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will print the URL of your API endpoint to the console similar to &lt;code&gt;https://random-string.execute-api.us-east-1.amazonaws.com&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add an Ember.js app.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages
pnpx ember-cli new web &lt;span class="nt"&gt;--lang&lt;/span&gt; en &lt;span class="nt"&gt;--skip-npm&lt;/span&gt; &lt;span class="nt"&gt;--skip-git&lt;/span&gt;
pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll remove the &lt;code&gt;ember-cli-sri&lt;/code&gt; package from the ember app as it's not working with the SST cloudfront deployment.&lt;br&gt;
In future ember releases, ember-cli &amp;gt; 5, it's likely the following step won't be needed. If it fails don't worry.&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;cd &lt;/span&gt;web &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pnpm remove ember-cli-sri
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add the web app to your stacks and link the API to it.
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;stacks/MyStack.tsc&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-import { StackContext, Api } from "sst/constructs";
&lt;/span&gt;&lt;span class="gi"&gt;+import { StaticSite, StackContext, Api } from "sst/constructs";
&lt;/span&gt;
export function API({ stack }: StackContext) {
  const api = new Api(stack, "api", {
    routes: {
      "GET /": "packages/functions/src/lambda.handler",
    },
  });

+ const web = new StaticSite(stack, "web", {
&lt;span class="gi"&gt;+   path: "packages/web",
+   buildOutput: "dist",
+   buildCommand: "pnpm build",
+   environment: {
+     EMBER_APP_API_URL: api.url,
+   }
+ });
+
&lt;/span&gt;  stack.addOutputs({
    ApiEndpoint: api.url,
&lt;span class="gi"&gt;+   Website: web.url,
&lt;/span&gt;  });
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Config the Ember.js app to use the API.
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;packages/web/config/environment.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;const ENV = {
&lt;/span&gt;&lt;span class="gi"&gt;+  apiUrl: process.env.EMBER_APP_API_URL,
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;packages/web/ember-cli-build.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;const app = new EmberApp(defaults, {
&lt;/span&gt;&lt;span class="gd"&gt;- // Add options here
&lt;/span&gt;&lt;span class="gi"&gt;+ storeConfigInMeta: false,
&lt;/span&gt;&lt;span class="err"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start the ember app locally and bind SST to it.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;packages/web
pnpm sst &lt;span class="nb"&gt;bind &lt;/span&gt;ember s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Remove the Ember.js welcome page and fetch the welcome from the hello endpoint.
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;packages/web/app/routes/application.js&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Route&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;@ember/routing/route&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;config&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;web/config/environment&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="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nx"&gt;ApplicationRoute&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;model&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;apiUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&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;&lt;em&gt;packages/web/app/templates/application.hbs&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;page-title&lt;/span&gt; &lt;span class="s2"&gt;"Web"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;body&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the API hello message rendered by the Ember.js app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy to production.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm sst deploy &lt;span class="nt"&gt;--stage&lt;/span&gt; prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congratulations you just deployed your app! The Website URL will be printed to the console.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean up
&lt;/h3&gt;

&lt;p&gt;To cleanup your AWS resources you can remove the development environment and the production environment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm sst remove
pnpm sst remove &lt;span class="nt"&gt;--stage&lt;/span&gt; prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>ember</category>
      <category>serverless</category>
      <category>fullstack</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Improving Ember.js serve and testing performance</title>
      <dc:creator>Ewan McDougall</dc:creator>
      <pubDate>Sun, 11 Oct 2020 07:31:11 +0000</pubDate>
      <link>https://dev.to/mrloop/improving-ember-js-serve-and-testing-performance-46in</link>
      <guid>https://dev.to/mrloop/improving-ember-js-serve-and-testing-performance-46in</guid>
      <description>&lt;p&gt;&lt;em&gt;This post &lt;a href="https://blog.mrloop.com/javascript/ember/typescript/2020/09/18/ember-serve.html" rel="noopener noreferrer"&gt;originally appeared on my personal blog.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I use the cli command &lt;code&gt;ember serve&lt;/code&gt; or the abbreviated form &lt;code&gt;ember s&lt;/code&gt; everyday, to build and serve locally ember apps for development. I noticed that &lt;code&gt;ember s --path dist&lt;/code&gt; and &lt;code&gt;ember t --path dist&lt;/code&gt; were taking a long time to start. The &lt;code&gt;--path&lt;/code&gt; flag lets you reuse an existing build at the given path.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;➜  ~ ember &lt;span class="nb"&gt;help&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"--path&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;^&lt;/span&gt;&lt;span class="se"&gt;\w&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-B1&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"--path"&lt;/span&gt;
ember serve &amp;lt;options...&amp;gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Path&lt;span class="o"&gt;)&lt;/span&gt; Reuse an existing build at given path.
ember &lt;span class="nb"&gt;test&lt;/span&gt; &amp;lt;options...&amp;gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;Path&lt;span class="o"&gt;)&lt;/span&gt; Reuse an existing build at given path.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Slow Continuous Integration
&lt;/h2&gt;

&lt;p&gt;This long start time also significantly increased the total run time of the continuous integration, CI, test run of the project I was working on. The CI build was split into a compile stage and a test stage. The compile stage fetches dependencies and compiles the app using &lt;code&gt;ember b&lt;/code&gt; and prepares the server side API for our full stack smoke tests. The app uses ember-cli-typescript, and the build can take around 2 minutes compiling typescript on the CI. The test stage consists of 7 concurrent jobs which all use the compiled assets from the compile stage. By having a single compile stage and 7 test stages the start time to end time will be quicker. The total run time will be slightly longer due to multiple 7 extra test environment boot steps.&lt;/p&gt;

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

&lt;p&gt;However the total run time for the CI test run increased more than could be accounted for by the extra boot steps, that is the total time for all the jobs to complete took around 14 minutes more than expected. Due to using concurrent jobs the time from starting the build and the build finishing reduces, there was a decrease in time to feedback but not what was expected and the total run time increased more than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when you run &lt;code&gt;ember t&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ember&lt;/code&gt; is the &lt;a href="https://cli.ember.com." rel="noopener noreferrer"&gt;command line interface for Ember.js&lt;/a&gt; &lt;code&gt;whereis ember&lt;/code&gt; will tell you where the executable is, in my case &lt;code&gt;~/.yarn/bin/ember&lt;/code&gt;. Opening &lt;code&gt;~/yarn/bin/ember&lt;/code&gt; the first line is &lt;code&gt;#!/usr/bin/env node&lt;/code&gt; telling us its a node app, which creates a ember-cli &lt;a href="https://github.com/ember-cli/ember-cli/blob/f1d58c22bb70b2c83626abff1e361e860661580d/lib/cli/index.js#L123" rel="noopener noreferrer"&gt;&lt;code&gt;CLI&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/ember-cli/ember-cli/blob/f1d58c22bb70b2c83626abff1e361e860661580d/lib/cli/index.js#L145" rel="noopener noreferrer"&gt;&lt;code&gt;run&lt;/code&gt;s&lt;/a&gt; it. Parsing the command line args and creating a Command object with in turn creates the appropriate Task object and runs it. In this case it's the &lt;a href="https://github.com/ember-cli/ember-cli/blob/069dbf3353daad643fc6bd46840b7d1435e41c2f/lib/tasks/test.js" rel="noopener noreferrer"&gt;TestTask&lt;/a&gt;, which when run invokes &lt;a href="https://github.com/testem/testem" rel="noopener noreferrer"&gt;testem&lt;/a&gt;, the JS test runner used by Ember.js. The TestTask also allows ember addons to &lt;a href="https://github.com/ember-cli/ember-cli/blob/069dbf3353daad643fc6bd46840b7d1435e41c2f/lib/tasks/test.js#L30" rel="noopener noreferrer"&gt;inject middleware&lt;/a&gt; into testem. This is very similar to the ServerTask, run when using &lt;code&gt;ember s&lt;/code&gt;, with also allows ember addons to &lt;a href="https://github.com/ember-cli/ember-cli/blob/836e89b4f26577cc9e492973048b9293b375c914/lib/tasks/server/express-server.js#L100" rel="noopener noreferrer"&gt;inject middleware&lt;/a&gt; into the development server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying slow build stage
&lt;/h2&gt;

&lt;p&gt;There's some useful documentation describing how to track down &lt;a href="https://github.com/ember-cli/ember-cli/blob/c9320aeb9fb521887bd8fec8f722bef0608c9fe5/docs/perf-guide.md#debug-logging" rel="noopener noreferrer"&gt;build performance issues here&lt;/a&gt; and in the &lt;a href="https://cli.emberjs.com/release/advanced-use/debugging/" rel="noopener noreferrer"&gt;ember-cli docs&lt;/a&gt;. Using one of the techniques described, I add a &lt;code&gt;DEBUG&lt;/code&gt; environment variable before &lt;code&gt;ember s&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="nv"&gt;$ DEBUG&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;ember-cli:&lt;span class="k"&gt;*&lt;/span&gt; ember s &lt;span class="nt"&gt;--path&lt;/span&gt; dist
  ember-cli:test-server isForTests: &lt;span class="nb"&gt;false&lt;/span&gt; +10s
  ember-cli:broccoli-watcher serving: /admin/index.html +37s
  ember-cli:broccoli-watcher serving: &lt;span class="o"&gt;(&lt;/span&gt;prefix stripped&lt;span class="o"&gt;)&lt;/span&gt; /index.html, was: /admin/index.html +1ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does it take 10s for isForTests, and what is broccoli-watcher doing, I thought we were serving precompiled assets? To dig down a bit more, and produce a lot more debug info ran &lt;code&gt;DEBUG=*:* ember s --path dist&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="nv"&gt;$ DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;:&lt;span class="k"&gt;*&lt;/span&gt; ember s &lt;span class="nt"&gt;--path&lt;/span&gt; dist
...
– Serving on http://localhost:4200/admin/
...
ember-cli-typescript:typecheck-worker Typecheck &lt;span class="nb"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;0 diagnostics&lt;span class="o"&gt;)&lt;/span&gt; +49s
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not &lt;code&gt;ember-cli:broccoli-watcher&lt;/code&gt; but &lt;code&gt;ember-cli-typescript:typecheck-worker&lt;/code&gt; taking up all the time. The real culprit is ember-cli-typescript middleware. Type checking is being performed even when the code is already transpiled, the case when using the &lt;code&gt;--path&lt;/code&gt; flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improving performance
&lt;/h2&gt;

&lt;p&gt;The code change to improve performance was &lt;a href="https://github.com/typed-ember/ember-cli-typescript/pull/1148/commits/df022189ffd144fe70ba7bb5551cc3fdde565f0c#diff-287bcabd2443a8d80ebe0fb212241315" rel="noopener noreferrer"&gt;small&lt;/a&gt;. When adding server middleware or testem middleware check the options passed to ember-cli and if they contain the path flag then don't run typechecking. A small &lt;a href="https://github.com/ember-cli/ember-cli/pull/9205" rel="noopener noreferrer"&gt;&lt;code&gt;ember-cli&lt;/code&gt; update&lt;/a&gt; was also required. ember-cli would pass all the CLI flags to the server middleware, but not to the testem middleware. Without the ember-cli update, ember-cli-typechecking would not of been able to perform the check when testem middleware was added. Interesting to note that for each PR I spent a lot more time figuring out how to effectively test the changes than implement them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fczzt9sf17aueiptvkpo5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fczzt9sf17aueiptvkpo5.png" alt="Decreased CI test time after performance improvement"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;These updates significantly sped up the CI build times, and more noticeably, improved my work flow by drastically reducing execution time of the ember commands when using the &lt;code&gt;--path&lt;/code&gt; flag.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ember</category>
      <category>typescript</category>
      <category>performance</category>
    </item>
    <item>
      <title>ReactJS WebExtensions</title>
      <dc:creator>Ewan McDougall</dc:creator>
      <pubDate>Fri, 04 Sep 2020 08:35:33 +0000</pubDate>
      <link>https://dev.to/mrloop/reactjs-webextensions-2al6</link>
      <guid>https://dev.to/mrloop/reactjs-webextensions-2al6</guid>
      <description>&lt;p&gt;&lt;em&gt;This post &lt;a href="https://blog.mrloop.com/javascript/reactjs/2020/08/21/web-ext-react.html" rel="noopener noreferrer"&gt;originally appeared on my personal blog.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I wanted to write a &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions" rel="noopener noreferrer"&gt;WebExtension&lt;/a&gt; for Firefox and Chrome in &lt;a href="https://reactjs.org" rel="noopener noreferrer"&gt;ReactJS&lt;/a&gt;, with little configuration in the simplest possible manner, using &lt;a href="https://reactjs.org/docs/create-a-new-react-app.html#create-react-app" rel="noopener noreferrer"&gt;create-react-app&lt;/a&gt; and &lt;a href="https://github.com/mozilla/web-ext" rel="noopener noreferrer"&gt;web-ext&lt;/a&gt;, I couldn't find any guides or instructions online so this is the setup I used. &lt;a href="https://reactjs.org/docs/create-a-new-react-app.html#create-react-app" rel="noopener noreferrer"&gt;create-react-app&lt;/a&gt; is the recommended tool for creating new single page applications in React.&lt;/p&gt;

&lt;p&gt;Lets create an app, first make sure you've got &lt;a href="https://nodejs.org/en/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; installed then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-react-app web-ext-react-hello
&lt;span class="nb"&gt;cd &lt;/span&gt;web-ext-react-hello
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have a basic react app running. The next step is to bundle it as a web extension. For this we will use &lt;a href="https://github.com/mrloop/web-ext-react" rel="noopener noreferrer"&gt;web-ext-react&lt;/a&gt;, the library I extracted from &lt;a href="https://github.com/mrloop/race-ext-react" rel="noopener noreferrer"&gt;race-ext-react&lt;/a&gt; to help bundle react apps as web extensions.&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 &lt;span class="nt"&gt;-D&lt;/span&gt; web-ext web-ext-react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A web extension can have multiple different javascripts, HTML and CSS for different parts. For example your web extension may have a sidebar or a popup, each with its own javascripts, HTML and CSS. As &lt;code&gt;create-react-app&lt;/code&gt; is designed to output a single app and not multiple, we need to conditionally invoke different components of our single react app depending on the context, be it the sidebar, popup, content script or background script. In this case we'll add a browser action popup. The &lt;code&gt;App&lt;/code&gt; component will be conditionally rendered if invoked from the browser action context.&lt;/p&gt;

&lt;h3&gt;
  
  
  src/index.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isBrowserAction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&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;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StrictMode&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;App&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="sr"&gt;/React.StrictMode&amp;gt;&lt;/span&gt;&lt;span class="err"&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extension needs a &lt;code&gt;manifest.json&lt;/code&gt;, create &lt;code&gt;extension/manifest.json&lt;/code&gt; and copy the logo to the extension directory &lt;code&gt;cp public/logo192.png extension&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  extension/manifest.json
&lt;/h3&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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bundle ReactJS as web extension"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-ext-react-hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"homepage_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;"https://github.com/mrloop/web-ext-react-hello"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icons"&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;"192"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logo192.png"&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;"browser_action"&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;"default_icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logo192.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello WebExt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_popup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"popup.html"&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 manifest declares a browser action with the react logo. This will appear in the browser toolbar when the extension is run. Clicking the icon and you'll see the popup running the &lt;code&gt;App&lt;/code&gt; component.&lt;/p&gt;

&lt;p&gt;To start the extension scripts can be added to &lt;code&gt;package.json&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  package.json
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start:firefox"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-ext-react run | xargs -L1 web-ext run -u http://www.example.org/ -s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start:chrome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"web-ext-react run | xargs -L1 web-ext run -u http://www.example.org/ -t chromium -s"&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;Tweak the styling, add &lt;code&gt;padding&lt;/code&gt; and changing &lt;code&gt;min-height&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  src/App.css
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.App-header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1em&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#282c34&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;200px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="m"&gt;2vmin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;white&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;Now run &lt;code&gt;yarn start:firefox&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;We now have the default &lt;code&gt;create-react-app&lt;/code&gt; running as a web extension! Try editing the app and live reload still works.&lt;/p&gt;

&lt;p&gt;For complete source code please visit &lt;a href="https://github.com/mrloop/web-ext-react-hello" rel="noopener noreferrer"&gt;https://github.com/mrloop/web-ext-react-hello&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webextension</category>
    </item>
  </channel>
</rss>
