<?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>Forem: bitcrowd</title>
    <description>The latest articles on Forem by bitcrowd (@bitcrowd).</description>
    <link>https://forem.com/bitcrowd</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%2Forganization%2Fprofile_image%2F2414%2Fea1a475c-a34a-4478-9ff4-dd2b2e695ca8.jpg</url>
      <title>Forem: bitcrowd</title>
      <link>https://forem.com/bitcrowd</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/bitcrowd"/>
    <language>en</language>
    <item>
      <title>Getting React on Rails to work with Turbo Streams</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 15 Jan 2026 09:30:54 +0000</pubDate>
      <link>https://forem.com/bitcrowd/getting-react-on-rails-to-work-with-turbo-streams-2c46</link>
      <guid>https://forem.com/bitcrowd/getting-react-on-rails-to-work-with-turbo-streams-2c46</guid>
      <description>&lt;p&gt;On a recent client project, redesigning a large &lt;a href="https://rubyonrails.org/" rel="noopener noreferrer"&gt;Rails&lt;/a&gt; app, we found ourselves having to get Rails, Hotwire's &lt;a href="https://turbo.hotwired.dev/" rel="noopener noreferrer"&gt;Turbo&lt;/a&gt; and &lt;a href="https://www.shakacode.com/react-on-rails/docs/" rel="noopener noreferrer"&gt;React on Rails&lt;/a&gt; to play together nicely. Each of the tools comes with its own amounts of &lt;em&gt;magic™&lt;/em&gt;, so integrating them turned out to be a bit of a juggle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario
&lt;/h2&gt;

&lt;p&gt;On this project, we were implementing the new design for a particular page. At it's core, it shows two lists, each with a "Load More" button to asynchronously fetch and append more items to it. The first list is just a stack of items, the second one also includes a grouping of the items. Imagine it roughly like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65l240dz5eyawo54w6dg.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%2F65l240dz5eyawo54w6dg.png" alt="Mockup of a page with two lists, List A and List B, each with a " width="800" height="1125"&gt;&lt;/a&gt;" button below. List B is divided into groups Group A, Group B and Group C"/&amp;gt;&lt;/p&gt;

&lt;p&gt;In "List A", clicking "Load More" should append another batch of 5 items to the bottom of the list until the bitter end. For "List B", clicking "Load More" should also append another batch of 5 items, where some may still belong to "Group C" and others will form new groups, each with their respective group header. The list items are heterogeneous and contain complex markup with graphics, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pagination
&lt;/h2&gt;

&lt;p&gt;We decided to implement the pagination logic using &lt;a href="https://turbo.hotwired.dev/reference/streams" rel="noopener noreferrer"&gt;Turbo Streams&lt;/a&gt;, as they seem to be a good fit for our use case. They also integrate nicely into the Rails ecosystem without being too tightly coupled (e.g. to a specific version of Rails). From Rails 7 on, Turbo also replaced Rails' UJS (Unobtrusive JavaScript) functionality and is the recommended tool for sprinkling asynchrony and SPA (Single Page Application)-feelings. From the Turbo Stream docs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Turbo Streams deliver page changes as fragments of HTML wrapped in &lt;code&gt;&amp;lt;turbo-stream&amp;gt;&lt;/code&gt; elements. Each stream element specifies an action together with a target ID to declare what should happen to the HTML inside it. These elements can be delivered to the browser synchronously as a classic HTTP response, or asynchronously […]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And further on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;They can be used to surgically update the DOM after a user action such as removing an element from a list without reloading the whole page […]&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This sounds like a match for what we are doing!&lt;/p&gt;

&lt;p&gt;By having our server respond with &lt;code&gt;turbo_stream&lt;/code&gt; templates for the pagination requests, we are able to use the same &lt;a href="https://viewcomponent.org/" rel="noopener noreferrer"&gt;View Components&lt;/a&gt; and partials we already have in place from the initial server-rendered page. This is important to us because we're already using our component library throughout the whole project. Reimplementing the same components in a different framework or language just for this single page would be unnecessary duplication, not to mention the constant risk of the two implementations diverging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example
&lt;/h3&gt;

&lt;p&gt;For the sake of simplicity, let's focus on the first list, "List A". The initially rendered page would have a markup like this for the list:&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;h2&amp;gt;&lt;/span&gt;List A&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"list-a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;abc-item/&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;abc-item/&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;abc-item/&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;abc-item/&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;abc-item/&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"load-more-a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;load-more-button/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user clicks the "Load More" button, we want to retrieve the next batch of items, append them to the list and replace the button with one to request the next batch. If there is no next batch of items, we remove the "Load More button.&lt;/p&gt;

&lt;p&gt;In the controller taking care of the pagination request, we can render a response template of the format &lt;code&gt;turbo_stream&lt;/code&gt;. A simplified version could look 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;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_stream.append&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;list-a&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;
&lt;span class="na"&gt;AbcItem.with_collection&lt;/span&gt;&lt;span class="err"&gt;(@&lt;/span&gt;&lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="na"&gt;as:&lt;/span&gt; &lt;span class="na"&gt;:item&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;pagy.next&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt;
&lt;span class="na"&gt;turbo_stream.replace&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;load-more-a&lt;/span&gt;&lt;span class="err"&gt;',&lt;/span&gt; &lt;span class="na"&gt;method:&lt;/span&gt; &lt;span class="na"&gt;:morph&lt;/span&gt; &lt;span class="na"&gt;do&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"load-more-a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt; &lt;span class="na"&gt;LoadMoreButton.new&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;path:&lt;/span&gt; &lt;span class="err"&gt;'/',&lt;/span&gt; &lt;span class="na"&gt;params:&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="na"&gt;page:&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;pagy.next&lt;/span&gt; &lt;span class="err"&gt;})&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;else&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%=&lt;/span&gt; &lt;span class="na"&gt;turbo_stream.remove&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="na"&gt;load-more-a&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;%&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt; &lt;span class="err"&gt;%&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the example is View Components and the &lt;a href="https://github.com/ddnexus/pagy" rel="noopener noreferrer"&gt;pagy&lt;/a&gt; pagination library. In the template, we're appending items to "List A" and replace the "Load More" button with a new one. If there aren't any items left the "Load More" button gets removed.&lt;/p&gt;

&lt;p&gt;The response from the server would then look roughly 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;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"append"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"list-a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;abc-item/&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"load-more-a"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It appends one item to the list and removes the button to load more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Our &lt;code&gt;&amp;lt;abc-item/&amp;gt;&lt;/code&gt; element contains a React component, &lt;code&gt;MyComponent&lt;/code&gt; in its markup, rendered via React on Rails' &lt;code&gt;react_component&lt;/code&gt; helper. In the final HTML those React components appear as three elements each. Two script tags with context and props and one wrapper element for the rendered HTML of the component:&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;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"js-react-on-rails-context"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- MyComponent markup --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"js-react-on-rails-component"&lt;/span&gt;
  &lt;span class="na"&gt;data-component-name=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent"&lt;/span&gt;
  &lt;span class="na"&gt;data-trace=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;data-dom-id=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For any &lt;code&gt;&amp;lt;abc-item/&amp;gt;&lt;/code&gt; element added to the page via Turbo Streams, those three elements are also added to the DOM, but the wrapping &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; stayed empty: &lt;strong&gt;the component wasn't actually mounted!&lt;/strong&gt; 🙀&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"js-react-on-rails-context"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt;
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;
  &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"js-react-on-rails-component"&lt;/span&gt;
  &lt;span class="na"&gt;data-component-name=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent"&lt;/span&gt;
  &lt;span class="na"&gt;data-trace=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;
  &lt;span class="na"&gt;data-dom-id=&lt;/span&gt;&lt;span class="s"&gt;"MyComponent-react-component-eaca5b56-0732-4674-b9da-f858c224d410"&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="err"&gt;…&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Turbo is injecting markup into the already fully rendered page and none of the DOM events React on Rails is hooking into to execute the JavaScript to mount React components on the page are being fired.&lt;/p&gt;

&lt;h3&gt;
  
  
  Options
&lt;/h3&gt;

&lt;p&gt;React on Rails already has &lt;a href="https://www.shakacode.com/react-on-rails/docs/building-features/turbolinks/#turbo-streams-requires-react-on-rails-pro" rel="noopener noreferrer"&gt;support for Turbo Streams&lt;/a&gt;, but only as part of their paid &lt;em&gt;pro&lt;/em&gt; plan. Going "pro" wasn't an option for the project as they are on a tight budget and only using small parts of React on Rails in isolated areas of their app. So we're not really qualifying for "pro" here…&lt;/p&gt;

&lt;p&gt;Server-side rendering the React components would also have been an option to tackle the problem. But this again was out of scope for our project and we did not want to run JavaScript on the server.&lt;/p&gt;

&lt;p&gt;So what now?&lt;/p&gt;

&lt;h2&gt;
  
  
  JS to the rescue
&lt;/h2&gt;

&lt;p&gt;With the help of some AI coding agents, we explored the internals of the &lt;a href="https://www.npmjs.com/package/react-on-rails" rel="noopener noreferrer"&gt;&lt;code&gt;react-on-rails&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/@hotwired/turbo-rails" rel="noopener noreferrer"&gt;@hotwired/turbo-rails&lt;/a&gt; NPM package to see what was actually happening. The rough idea was that, if any events were emitted by Turbo when Stream elements were injected and rendered, we could hook into them and then manually call React on Rails to mount all the newly inserted components on the page.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;Turbo&lt;/code&gt;, we found the &lt;a href="https://github.com/hotwired/turbo/blob/d069101e701f84369689d8fbd521d7a13ca971a0/src/elements/stream_element.js" rel="noopener noreferrer"&gt;&lt;code&gt;StreamElement&lt;/code&gt;&lt;/a&gt;. It has a &lt;code&gt;render&lt;/code&gt; function and a &lt;code&gt;target&lt;/code&gt; pointing to the ID of the DOM-element it's rendered to. Combining these two things, the plan was to extend the &lt;code&gt;render&lt;/code&gt; function to also take care of mounting any React elements within the updated target element. This means, when the &lt;code&gt;StreamElement&lt;/code&gt; contained any React components, we want to make sure they are correctly mounted &lt;strong&gt;after&lt;/strong&gt; the rendering of the &lt;code&gt;StreamElement&lt;/code&gt; itself was done.&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="cm"&gt;/**
 * Gets the current target (an element ID) to which the result will
 * be rendered.
 */&lt;/span&gt;
&lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;target&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;target&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So we wrote our own tiny JS module to patch Turbo with some additional logic for our React on Rails integration:&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="c1"&gt;// First we get hold of the original render function of Turbo's StreamElement&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalRender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Turbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StreamElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Then we override it to extend it with our own additional logic:&lt;/span&gt;
&lt;span class="nx"&gt;Turbo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StreamElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// First we call the original render function&lt;/span&gt;
  &lt;span class="nx"&gt;originalRender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Then we get hold of the target element&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;target&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="c1"&gt;// Then we wait for the next frame, to make sure that the DOM is fully loaded&lt;/span&gt;
  &lt;span class="c1"&gt;// and we call a function to have React on Rails mount all components within&lt;/span&gt;
  &lt;span class="c1"&gt;// the newly inserted DOM snippet.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;mountReactOnRailsComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The custom &lt;code&gt;mountReactOnRailsComponents&lt;/code&gt; function is pretty much a manual re-implementation of what's happening when React on Rails is client-side rendering components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mountReactOnRailsComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domNode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;domNode&lt;/span&gt;
    &lt;span class="c1"&gt;// First we get all React on Rails components within our node&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script.js-react-on-rails-component&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Next we get the name of the component and the DOM ID of the placeholder&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;componentName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;domId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shouldHydrate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hydrate&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Then we put together the component props&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ReactOnRails props JSON parse error for&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Now we need the placeholder element&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;placeholder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// We check whether the component was already mounted&lt;/span&gt;
      &lt;span class="c1"&gt;// in this case we don't want to re-render it.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasMounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reactOnRailsMounted&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasMounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="c1"&gt;// Then we call React on Rails' render function to mount the component&lt;/span&gt;
      &lt;span class="nx"&gt;ReactOnRails&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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shouldHydrate&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Finally, we mark the placeholder as mounted, so that we don't accidentally&lt;/span&gt;
      &lt;span class="c1"&gt;// re-render it.&lt;/span&gt;
      &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reactOnRailsMounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;true&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;With this in place, our Turbo Streams based pagination works like charm. Any React component newly added to the DOM is mounted as normal 🎉.&lt;/p&gt;

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

&lt;p&gt;Drilling around like this in the internals of Turbo and React on Rails of course yields its risks. Now we're maintaining additional code which may break any time we update our Turbo or React on Rails dependences. But as we're only using the module in a small isolated area of the application, it's a risk we can accept for now. Our test coverage of the feature will inform us early enough.&lt;/p&gt;

&lt;p&gt;Using open source technologies, we have the chance to peak under the hood of the tools and can if necessary adapt them to our needs. And working with legacy codebases and legacy Rails apps, their may be situations with unconventional needs…&lt;/p&gt;

&lt;p&gt;We hope this post was helpful and inspires you to explore the details of the tools you're using the next time you hit a wall with them. Migrating to an entirely different tool is often a way bigger operation, especially in large monoliths.&lt;/p&gt;

</description>
      <category>redesign</category>
      <category>react</category>
      <category>rails</category>
      <category>turbo</category>
    </item>
    <item>
      <title>Loading Chrome extensions for development in 2025</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Wed, 03 Dec 2025 08:25:24 +0000</pubDate>
      <link>https://forem.com/bitcrowd/loading-chrome-extensions-for-development-in-2025-1039</link>
      <guid>https://forem.com/bitcrowd/loading-chrome-extensions-for-development-in-2025-1039</guid>
      <description>&lt;p&gt;For loading a development build of our open source browser extension &lt;a href="https://github.com/bitcrowd/tickety-tick" rel="noopener noreferrer"&gt;Tickety-Tick&lt;/a&gt; into Google Chrome during local development, we used to use Chrome's handy &lt;code&gt;--load-extension&lt;/code&gt; flag. It allowed to load a local unpacked build of the extension via the command line. As this feature was &lt;a href="https://developer.chrome.com/blog/extension-news-june-2025#removing_the_--load-extension_flag" rel="noopener noreferrer"&gt;recently removed&lt;/a&gt; from all branded builds of Chrome, we needed to update our workflow to use the-new-way™ - "remote debugging pipes".&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Tickety-Tick is on the nichier side of tech within our fleet of open source projects: it's a browser extension for establishing conventions on naming branches and writing commit messages in Git. And while we're slightly leaning towards team Firefox among us developers, Tickety-Tick is of course also supporting Google Chrome (and others…). For this, we as developers have to be able to run a local build of the extension in Chrome during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The old way
&lt;/h2&gt;

&lt;p&gt;For local development, we had a &lt;code&gt;yarn open:chrome&lt;/code&gt; command which would open Chrome with the current local build of the extension already installed, ready to click around. Under the hood, we utilized Google's &lt;a href="https://github.com/GoogleChrome/chrome-launcher" rel="noopener noreferrer"&gt;chrome-launcher&lt;/a&gt; package to start a fresh, unpersonalized instance of Chrome for us. The code looked about like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chromeFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ChromeLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defaultFlags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--disable-extensions&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="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-default-browser-check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--load-extension=/dist/chrome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;ChromeLauncher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;chromeFlags&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chrome running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is the &lt;code&gt;--load-extension&lt;/code&gt; flag which points to the local Chrome build of the extension.&lt;/p&gt;

&lt;p&gt;Some day, this just stopped working. Chrome would start as usual, but without the browser extension loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened?
&lt;/h2&gt;

&lt;p&gt;Some debugging and internet research brought us to &lt;a href="https://groups.google.com/a/chromium.org/g/chromium-extensions/c/aEHdhDZ-V0E/m/UWP4-k32AgAJ" rel="noopener noreferrer"&gt;this post&lt;/a&gt; by the Chrome team from March 2025:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RFC: Removing the &lt;code&gt;--load-extension&lt;/code&gt; flag in branded Chrome builds&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Turns out the &lt;code&gt;--load-extension&lt;/code&gt; flag was commonly abused by malicious parties and the Chrome team was looking into ways to further restrict this. It lead them to removing this feature from all branded Chrome builds (so Chromium would still have it…) and instead promote other means for loading extensions for testing and development.&lt;/p&gt;

&lt;h2&gt;
  
  
  What now?
&lt;/h2&gt;

&lt;p&gt;To figure out the new™ way to load an unpacked, locally build extension in Chrome, we just naively searched for the &lt;code&gt;--load-extension&lt;/code&gt; flag on GitHub, hoping to find hints how other people updated their code. A promising hit was in the &lt;code&gt;chrome-launcher&lt;/code&gt; package itself. If somebody knew how to do it, they should be the ones. The searching pointed us to a really helpful code comment in their tests:&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="c1"&gt;// Note: --load-extension in chromeFlags used to be the primary method of&lt;/span&gt;
&lt;span class="c1"&gt;// loading extensions, but this is removed from official stable Chrome builds&lt;/span&gt;
&lt;span class="c1"&gt;// starting from Chrome 137. This shows the officially supported way to load&lt;/span&gt;
&lt;span class="c1"&gt;// extensions, with --remote-debugging-pipe.&lt;/span&gt;
&lt;span class="c1"&gt;// See: "Removing the `--load-extension` flag in branded Chrome builds"&lt;/span&gt;
&lt;span class="c1"&gt;// https://groups.google.com/a/chromium.org/g/chromium-extensions/c/aEHdhDZ-V0E/m/UWP4-k32AgAJ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As this was in a &lt;code&gt;load-extension-test.ts&lt;/code&gt; file, we figured we'd just try to adopt what they are doing in their test setup for our &lt;code&gt;open:chrome&lt;/code&gt; script. And as the comment indicates, we need to utilize the &lt;code&gt;--remote-debugging-pipe&lt;/code&gt; flag, something we already knew from &lt;a href="https://github.com/bitcrowd/chromic_pdf" rel="noopener noreferrer"&gt;ChromicPDF&lt;/a&gt; our PDF rendering library for Elixir based on headless Chrome.&lt;/p&gt;

&lt;p&gt;Following pretty much exactly what the &lt;code&gt;loadExtension via remote-debugging-pipe&lt;/code&gt; test was doing, we ended up with an updated &lt;code&gt;open-in-chrome&lt;/code&gt; script which would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start Chrome with the respective flags
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chromeFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defaultFlags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
     &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--disable-extensions&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="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--remote-debugging-pipe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--enable-unsafe-extension-debugging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-first-run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-default-browser-check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;]);&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;chromeFlags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;ignoreDefaultFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;startingUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="p"&gt;};&lt;/span&gt;

   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chrome&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;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Get hold of the debugging pipes
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remoteDebuggingPipes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chrome did not expose remoteDebuggingPipes&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Put together the command to load the extension
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Extensions.loadUnpacked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dir&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;ol&gt;
&lt;li&gt;Send the request and listen on the pipes
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

     &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
     &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
       &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pipe closed before response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
     &lt;span class="p"&gt;);&lt;/span&gt;

     &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
       &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&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;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
       &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;x00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
         &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
           &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
             &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
           &lt;span class="p"&gt;}&lt;/span&gt;
         &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
           &lt;span class="c1"&gt;// ignore non-JSON noise&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="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;x00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Check that the request was successful
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;firstResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to load extension: &lt;/span&gt;&lt;span class="p"&gt;${&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that a fresh Chrome should be running with our extension loaded! 🎉&lt;/p&gt;

&lt;h3&gt;
  
  
  Our script
&lt;/h3&gt;

&lt;p&gt;In our final script, we also added some emoji-powered checks and debugging statements, so that the CLI output aligns with our other tasks and the webpack build. Here's our final script, maybe you can draw inspiration from it for your own usecase:&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="cp"&gt;#!/usr/bin/env node
&lt;/span&gt;
&lt;span class="c1"&gt;// usage: open-in-chrome [extension-dir] [starting-url]&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;launcher&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chrome-launcher&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&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="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;chrome&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/bitcrowd/tickety-tick&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;launchChrome&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chromeFlags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defaultFlags&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--disable-extensions&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="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--remote-debugging-pipe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--enable-unsafe-extension-debugging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-first-run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--no-default-browser-check&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;chromeFlags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ignoreDefaultFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;startingUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chrome&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;launcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;⚠️ Expected remote-debugging-pipe mode on port 0, but got a debug port.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remoteDebuggingPipes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Chrome did not expose remoteDebuggingPipes&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🚀 Chrome launched with remote-debugging-pipe.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`📂 Loading extension from: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&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="nx"&gt;e6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Extensions.loadUnpacked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dir&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// --- Send command and wait for response&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pipe closed before response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;incoming&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunk&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;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;x00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// ignore non-JSON noise&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="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outgoing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;x00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;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;firstResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Failed to load extension: &lt;/span&gt;&lt;span class="p"&gt;${&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✅ Extension loaded (id: &lt;/span&gt;&lt;span class="p"&gt;${&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`🌐 Opening: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;💨 Chrome closed.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;launchChrome&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;❌ Error:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;p&gt;Comments in code can make other people's lives significantly easier. Think about it the next time you're hesitating to write one!&lt;/p&gt;

&lt;p&gt;And: searching GitHub for something as specific as a Chrome flag, can yield helpful results in any kind of file, even in tests.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>browserextension</category>
      <category>opensource</category>
      <category>development</category>
    </item>
    <item>
      <title>Meet Tickety-Tick</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Mon, 04 Nov 2024 11:01:57 +0000</pubDate>
      <link>https://forem.com/bitcrowd/meet-tickety-tick-2j68</link>
      <guid>https://forem.com/bitcrowd/meet-tickety-tick-2j68</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/bitcrowd/tickety-tick" rel="noopener noreferrer"&gt;Tickety-Tick&lt;/a&gt; is a tool so essential and at the same time so natural and obvious for our work at bitcrowd, that we actually never really introduced it much. So here is a very late &lt;em&gt;“meet Tickety-Tick”&lt;/em&gt;, a tool we love, take for granted and whose usage is part of the collective muscle memory at bitcrowd since what feels like forever ❤️.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basics
&lt;/h2&gt;

&lt;p&gt;Tickety-Tick is quite an exotic member of the fleet of &lt;a href="https://github.com/bitcrowd" rel="noopener noreferrer"&gt;open source projects at bitcrowd&lt;/a&gt;: itʼs a &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions" rel="noopener noreferrer"&gt;browser extension&lt;/a&gt; to generate Git branch names and commit messages for the ticket one is working on. It allows us to align branch and commit message naming patterns throughout all projects and have a fixed and easy to follow relationship between the ticket - may it be a bug description or a feature - and its corresponding change in the source code, regardless of the ticketing system used and its possibilities or impossibilities to integrate with code repositories.&lt;/p&gt;

&lt;p&gt;The idea is based on the &lt;a href="https://en.wikipedia.org/wiki/Convention_over_configuration" rel="noopener noreferrer"&gt;&lt;em&gt;“Convention over Configuration”&lt;/em&gt;&lt;/a&gt; design pattern which used to be super popular in the &lt;a href="https://www.ruby-lang.org/en/community/" rel="noopener noreferrer"&gt;Ruby community&lt;/a&gt; where bitcrowd hung out too in 2015 when the project was born. You agree upon a convention on how to name branches and commit messages and then have a tool in place to take care of enforcing this convention without you ever having to think about it or alternative ways to name branches and commit messages ever again.&lt;/p&gt;

&lt;p&gt;If your into history, there is also a &lt;a href="https://speakerdeck.com/bitboxer/tickety-tick" rel="noopener noreferrer"&gt;slidedeck&lt;/a&gt; &lt;a href="https://bodo.tasche.me/" rel="noopener noreferrer"&gt;Bodo&lt;/a&gt;, the original author of the browser extension, used for a lightning talk at &lt;a href="https://2016.rubyconf.pt/" rel="noopener noreferrer"&gt;Ruby Conf Portugal&lt;/a&gt; in 2016. I was in the audience back then and immediately sold on the idea 🥰.&lt;/p&gt;

&lt;h3&gt;
  
  
  Convention
&lt;/h3&gt;

&lt;p&gt;At bitcrowd, branch names follow the format of &lt;code&gt;&amp;lt;type&amp;gt;/&amp;lt;id&amp;gt;-&amp;lt;title&amp;gt;&lt;/code&gt; and the subject of the first commit message on that branch would be &lt;code&gt;[#&amp;lt;id&amp;gt;] &amp;lt;title&amp;gt;&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;&lt;/strong&gt;: the type of ticket&lt;/p&gt;

&lt;p&gt;This may be &lt;code&gt;feature&lt;/code&gt;, &lt;code&gt;bug&lt;/code&gt;, &lt;code&gt;task&lt;/code&gt;, &lt;code&gt;chore&lt;/code&gt; or whatever else the ticketing system uses.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;id&lt;/code&gt;&lt;/strong&gt;: the identifier of the ticket in the ticketing system&lt;/p&gt;

&lt;p&gt;This may be &lt;code&gt;TT-5045&lt;/code&gt; in &lt;a href="https://www.atlassian.com/software/jira" rel="noopener noreferrer"&gt;Jira&lt;/a&gt;, a numeric ID like on &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, a UUID on &lt;a href="https://www.notion.so/" rel="noopener noreferrer"&gt;Notion&lt;/a&gt;, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;title&lt;/code&gt;&lt;/strong&gt;: the title or subject of the issue&lt;/p&gt;

&lt;p&gt;On GitHub, this would for instance be the issue title, something like &lt;em&gt;“Add support for Safari”&lt;/em&gt;. When used for the branch name, the title gets lowercased and dasherized to &lt;code&gt;add-support-for-safari&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So for an imaginary feature ticket about adding support for Safari, Tickety-Tick would generate the branch name &lt;code&gt;feature/tt-504-add-support-for-safari&lt;/code&gt; and a commit message subject &lt;code&gt;[#TT-504] Add support for Safari&lt;/code&gt;, when used with the default configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Origin
&lt;/h3&gt;

&lt;p&gt;The convention was derived from existing workflows at bitcrowd. And while having aligned branch names already makes sense for collaboration alone, the aligned commit message subject is a bit more special. Itʼs useful for us at bitcrowd, because we squash-merge 99.9% of our pull requests. This means each merged pull request is represented by a single commit in the Git history. Respectively, each ticket we work on results in a single commit and the history can be read as something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[#504] Add support for Safari
[#503] Fix installation instructions for Chrome
[#501] Remove cookie banner
…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also use the generated subject as the title for our pull or merge requests and for time-tracking. But more on that later…&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow
&lt;/h2&gt;

&lt;p&gt;Letʼs walk though a possible workflow together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup
&lt;/h3&gt;

&lt;p&gt;First we need to add the Tickety-Tick web extension to our browser. Install it via &lt;a href="https://addons.mozilla.org/firefox/addon/tickety-tick/" rel="noopener noreferrer"&gt;Firefox Add-ons&lt;/a&gt;, the &lt;a href="https://chrome.google.com/webstore/detail/ciakolhgmfijpjbpcofoalfjiladihbg" rel="noopener noreferrer"&gt;Chrome Web Store&lt;/a&gt; or follow the &lt;a href="https://github.com/bitcrowd/tickety-tick?tab=readme-ov-file#installation" rel="noopener noreferrer"&gt;manual installation instructions&lt;/a&gt; for Chrome, Opera, Firefox or Safari. Then we need an ticket to work on. Tickety-Tick supports a constantly growing range of &lt;a href="https://github.com/bitcrowd/tickety-tick?tab=readme-ov-file#supported-ticket-systems" rel="noopener noreferrer"&gt;ticketing systems&lt;/a&gt; from GitHub to YouTrack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage
&lt;/h3&gt;

&lt;p&gt;Letʼs say weʼre working on Tickety-Tick itself. We have the project checked out locally, are on the latest changes of the &lt;code&gt;main&lt;/code&gt; branch and have a terminal window open in the projectʼs root directory. Then we browse the open issues on &lt;a href="https://github.com/bitcrowd/tickety-tick/issues" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ficvat6mf9dtiu78fv0jc.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%2Ficvat6mf9dtiu78fv0jc.png" alt="Screenshot of browsing the issues page of the Tickety-Tick repository on GitHub" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We want want to work on the first issue to migrate the ESLint configuration &lt;em&gt;(itʼs already assigned to me…)&lt;/em&gt; and click on the issue to read what needs to be done:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0n39bwsxel9tqxdnzrd.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%2Fc0n39bwsxel9tqxdnzrd.png" alt="Screenshot of the issue page for ticket number 427 with the title “Migrate ESLint config to flat format”" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The description is only pointing to a series of blog posts from the ESLint team explaining the new default configuration format. Letʼs assume we read it and are ready to start working on the ticket. So we open the Tickety-Tick browser extension either by clicking on the ticket icon in our browserʼs tool-bar or, if weʼre already among the power users, use the nifty ctrl t keyboard shortcut to open the extension popup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5a2wenb1hp5i62jo9mvt.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%2F5a2wenb1hp5i62jo9mvt.png" alt="Screenshot of the issue page for ticket number 427 with the title “Migrate ESLint config to flat format” and the Tickety-Tick browse extension popup window open" width="800" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It offers three buttons: one to copy the generated branch name to the clipboard, one to copy the generated commit message and one to copy the &lt;em&gt;“command”&lt;/em&gt;. We click the last button, and paste into our terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="s1"&gt;'feature/427-migrate-eslint-config-to-flat-format'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;--allow-empty&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s1"&gt;'[#427] Migrate ESLint config to flat format

https://github.com/bitcrowd/tickety-tick/issues/427
'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Power users / keyboard lovers can also tab through the buttons of course. No need to reach the popup window with the mouse.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Broken down into pieces, this will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Check out a new branch with the name &lt;code&gt;feature/427-migrate-eslint-config-to-flat-format&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create an empty commit with this message:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   [#427] Migrate ESLint config to flat format

   https://github.com/bitcrowd/tickety-tick/issues/427
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Check &lt;a href="https://bitcrowd.dev/a-note-on-commit-messages" rel="noopener noreferrer"&gt;our post on commit messages&lt;/a&gt; for more info about commit messages, subject vs. body, formatting, etc.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With that, we can start coding away. Whether we decide to later &lt;a href="https://docs.github.com/en/pull-requests/committing-changes-to-your-project/creating-and-editing-commits/changing-a-commit-message#rewriting-the-most-recent-commit-message" rel="noopener noreferrer"&gt;amend&lt;/a&gt; the initial commit, add our changes and write a proper commit message body, or if we just keep the empty commit as a starting point for a series of smaller commits afterwards, that depends on the use case and personal preference.&lt;/p&gt;

&lt;p&gt;I personally only amend the empty commit with my changes if the changes are super small. Otherwise, I prefer to have a series of commits which follow my thought process and just keep the empty commit as a starting point for my pull request.&lt;/p&gt;

&lt;h4&gt;
  
  
  Done?
&lt;/h4&gt;

&lt;p&gt;Once done with the implementation, we can open a pull request with our changes. If our changes are small enough to fit into a single commit, GitHub will even pre-fill the pull request title with the commit messageʼs subject &lt;code&gt;[#427] Migrate ESLint config to flat format&lt;/code&gt; and the description with the commit message body we provided.&lt;/p&gt;

&lt;p&gt;We at bitcrowd reuse the first commit messageʼs subject for the pull request title. It has proven to make browsing the list of open pull requests easier to grasp for our colleagues. So again a convention…&lt;/p&gt;

&lt;h3&gt;
  
  
  Habit
&lt;/h3&gt;

&lt;p&gt;For pretty much every dev team that we introduced to Tickety-Tick as part of our consulting work, using the tool became muscle memory quite fast. Especially the ctrl t shortcut plus tab quickly wired up as the go-to move whenever people would start working on a ticket or want to check out branches of a currently worked on ticket from another team member. Having the naming convention established and enforced, frees the developers from having to spent any attention or brain power onto them. Pick a ticket, read and understand it and paste a command to check out a branch for the ticket to start implementing it.&lt;/p&gt;

&lt;p&gt;When adapting Tickety-Tick as a team with already established rules around branch names and commit messages, it may make sense to look into the &lt;a href="https://github.com/bitcrowd/tickety-tick?tab=readme-ov-file#advanced-configuration" rel="noopener noreferrer"&gt;advanced configuration&lt;/a&gt; of Tickety-Tick. It allows to adjust the naming patterns to the teamʼs needs, so that the tool can be used to enforce already existing conventions. The configuration options are powerful enough to cover any naming pattern your team may have come up with. Yet they are simple and limited enough to not have you spent hours on them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;bitcrowd&lt;/strong&gt; can help you to optimize your workflows and team organization! Just &lt;a href="//mailto:info@bitcrowd.net"&gt;hit us up&lt;/a&gt; and we see what we can do for you!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why use it?
&lt;/h2&gt;

&lt;p&gt;So if you already have a ticketing system and a place to host your repositories, why should especially a browser extension between them be of so much value for you as a developer and/or your team? Arenʼt you already keeping track of your work and juggling around too many tools?&lt;/p&gt;

&lt;p&gt;In that regard, Tickety-Tick is &lt;strong&gt;not &lt;em&gt;another tool™&lt;/em&gt;&lt;/strong&gt; in the garage of tools youʼre already using. Itʼs more like a loose bandage to tie your tools together, so that they donʼt drift apart. As a side-effect, it may also loosely bandage your team members together, so that they donʼt fully drift apart concerning naming conventions…&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforce conventions
&lt;/h3&gt;

&lt;p&gt;Adapting Tickety-Tick helps to enforce conventions, which frees developers from having to think about these naming questions themselves day to day. This reduces the cognitive load when working with ticketing systems and Git. And this freed head-space is something both, a dev team or an individual developer on their own can benefit from.&lt;/p&gt;

&lt;p&gt;Having the naming done automatically via an agreed upon set of rules, also makes the project easier accessible for new people. Plus, it helps to keep things nice and tidy on the long run, without having to constantly have an eye on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Traceability
&lt;/h3&gt;

&lt;p&gt;Another huge and probably &lt;strong&gt;the most important benefit&lt;/strong&gt; of using Tickety-Tick is the connection between ticket and code contribution that it helps to establish. Combined with a habit of squash-merging pull requests, the team has a linear and easy to follow Git history: changes in code point to their commit and the commit message points to the ticket on the tracker. Looking at a commit message then allows to quickly dive into further detail about the change on the ticketing system, to find out where a feature originated from, etc. The quality and usefulness of this connection of course also depends on the quality of ticket descriptions, but thatʼs a whole different topic…&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F96c76bv91bk3barevfvd.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%2F96c76bv91bk3barevfvd.png" alt="Screenshot of a terminal window showing the Git history with pointers to issue IDs in subjects" width="800" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having this fixed connection between ticket and source code and having all people in the team know about it, turned out to be extremely beneficial for pretty much every project or team weʼve seen so far. Itʼs an additional mean for knowledge sharing, makes onboarding newcomers on the codebase faster and is generally a trusted reference for any member of the team, newcomer to long term contributer. Itʼs an additional source of documentation for the code with a level of detail which would not be possible within the code itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Further usage
&lt;/h3&gt;

&lt;p&gt;The encoded information about the connection between a ticket on the tracker and the code which later was written for it, can be used for further applications, like timetracking, work reports, etc.&lt;/p&gt;

&lt;p&gt;A whole new information heaven can be reached once &lt;a href="//../2024-06-10-how-even-the-simplest-RAG-can-empower-your-team/index.md"&gt;RAG&lt;/a&gt; (retrieval-augmented generation) comes into play. Looking at both, the ticket description and the resulting code contribution, a RAG system can learn a lot about the domain of a project and how the team communicates. Teams who adapted Tickety-Tick early enough in their work, now have a huge individual dataset of how they work right in front of them! This opens up quite some possibilities for knowledge sharing, analysis, automation and what not. We are super excited about the topic ourself and are currently doing a &lt;a href="https://bitcrowd.dev/tags/rag/" rel="noopener noreferrer"&gt;series of blog posts&lt;/a&gt; on it!&lt;/p&gt;

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

&lt;p&gt;After all the praise, the only thing left to say is: Just &lt;a href="https://github.com/bitcrowd/tickety-tick?tab=readme-ov-file#installation" rel="noopener noreferrer"&gt;try it&lt;/a&gt; out yourself! Tickety-Tick works in all major browsers people use nowadays (Firefox, Chrome, Opera, Safari). It supports a lot of different ticketing systems and if yours is not &lt;a href="https://github.com/bitcrowd/tickety-tick?tab=readme-ov-file#supported-ticket-systems" rel="noopener noreferrer"&gt;in the list&lt;/a&gt; yet, we can help you to put together a pull request for it. Weʼre always curious to find out which ticketing systems and workflows people use.&lt;/p&gt;

&lt;p&gt;If you have more questions about browser extensions or workflows, just hit us up!&lt;/p&gt;

</description>
      <category>git</category>
      <category>browserextensions</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Designing Beautiful and Accessible UX: Colour Contrast</title>
      <dc:creator>Ed Eva</dc:creator>
      <pubDate>Thu, 29 Aug 2024 14:27:38 +0000</pubDate>
      <link>https://forem.com/bitcrowd/designing-beautiful-and-accessible-ux-colour-contrast-j5o</link>
      <guid>https://forem.com/bitcrowd/designing-beautiful-and-accessible-ux-colour-contrast-j5o</guid>
      <description>&lt;p&gt;At the beginning of 2025, new EU laws for web accessibility will come into force, requiring industries and public bodies to ensure their websites are accessible to all users. This brings up a common question: how can we stick to by accessibility rules while still creating a good-looking product? In this post, we will focus on colour contrast, which is not only one of the simplest and most effective ways to enhance accessibility in design but also one of the most common accessibility fails.&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%2F0b8augw467751o1sbgpf.jpg" 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%2F0b8augw467751o1sbgpf.jpg" alt="Section of a website Web Sustainability Guidelines with colourful,&amp;lt;br&amp;gt;
  high-contrast buttons to select categories including Web Development, Hosting&amp;lt;br&amp;gt;
  and Infrastructure, Business Product Strategy, and UX Design. Hosting and&amp;lt;br&amp;gt;
  Infrastructure is selected with a purple fill. There is a small button to view&amp;lt;br&amp;gt;
  all guidelines at the bottom left which is black with white&amp;lt;br&amp;gt;
  text." width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://sustainablewebdesign.org" rel="noopener noreferrer"&gt;Sustainablewebdesign.org&lt;/a&gt; uses high contrast colourful design to great effect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick introduction to accessibility
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/WAI/" rel="noopener noreferrer"&gt;Web Accessibility Initiative (WAI)&lt;/a&gt; defines accessibility as the practice of making websites usable by people of all abilities and disabilities. This means designing and developing digital content that can be easily navigated and understood by everyone, regardless of their hardware, software, language, location, or ability.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Colour Contrast and Why It Matters
&lt;/h2&gt;

&lt;p&gt;Colour contrast refers to the difference in light between text (or any other foreground element) and its background. High colour contrast improves legibility, which is crucial for users with visual impairments, such as those with low vision or colour blindness.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html" rel="noopener noreferrer"&gt;Web Content Accessibility Guidelines (WCAG)&lt;/a&gt; set the (AA level) standards for colour contrast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The minimum contrast ratio for regular text is 4.5:1.&lt;/li&gt;
&lt;li&gt;For large-scale text (18 point or 14 point bold), the minimum ratio is 3:1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don’t need to memorise these ratios; there are plenty of tools available to help you check and adjust colour contrast, which we will mention later.&lt;/p&gt;

&lt;p&gt;So who is this for? Consider that in Europe alone, an average of 1 in 30 people experience sight loss, which means over 30 million individuals. Globally, millions more live with varying degrees of visual impairment. It's important to note that visual impairment exists on a spectrum: It can be permanent, affecting a relatively small number of people. It can be temporary or curable, as in the case of cataracts. And it can even be momentary, such as when someone forgets their glasses or struggles with bright sunlight. Ensuring adequate colour contrast can make a significant difference in the user experience for all these groups.&lt;/p&gt;

&lt;p&gt;The advantages of good colour contrast extend beyond this group to benefit the user experience of everyone. Here’s how it helps all your users:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Legibility: Clear, legible text increases users' ability to absorb information quickly, reducing cognitive load. (This is especially beneficial for users with conditions affecting short-term memory or focus.)&lt;/li&gt;
&lt;li&gt;Trust: Hard-to-read text can make users suspicious as it is reminiscent of the kind of fine print found on dubious products and contracts.&lt;/li&gt;
&lt;li&gt;Bright Conditions: High contrast is crucial for readability in bright environments, a common scenario for mobile users.&lt;/li&gt;
&lt;li&gt;User Retention: Easy-to-read content helps retain users, as they are less likely to become frustrated and leave the site.&lt;/li&gt;
&lt;li&gt;Error Prevention: Using colour contrast effectively can help users distinguish between different types of content and actions, reducing errors.&lt;/li&gt;
&lt;/ul&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%2Fjja1basw25irqpfqdnzp.jpg" 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%2Fjja1basw25irqpfqdnzp.jpg" alt="A comparison of two layouts each with a bold title, some copy text and a&amp;lt;br&amp;gt;
  button. The left layout, labeled High contrast ratio, has black text on a&amp;lt;br&amp;gt;
  white background. The button is a mid-tone blue with white text. The right&amp;lt;br&amp;gt;
  layout, labeled Low contrast ratio, has a blue title and light grey copy text&amp;lt;br&amp;gt;
  on a white background. The button is light blue with white&amp;lt;br&amp;gt;
  text." width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Information is absorbed quicker when it is clear and legible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility &amp;amp; Aesthetics
&lt;/h2&gt;

&lt;p&gt;Creating accessible designs doesn't mean compromising on aesthetics. In fact, integrating accessibility considerations can enhance the overall beauty and functionality of your design. By thoughtfully incorporating colour contrast and other accessibility principles, you can create visually appealing designs that are inclusive and user-friendly. Here are some practical tips and tools we use to achieve this balance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start Early: Consider contrast ratios during the early wireframing stages. Use appropriate shades of grey (or whatever colour you wireframe with) to distinguish elements.&lt;/li&gt;
&lt;li&gt;Palette Selection: Choose colours that include a dark, mid, and light shade with good contrast against each other. Using these tonal sets can help your colours sing. Tools that help find complementary, analogous, or triadic colour schemes can also be beneficial - &lt;a href="https://color.adobe.com/create/color-wheel" rel="noopener noreferrer"&gt;Adobe Color&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Using Colour to Highlight: Even though colour contrast refers to the contrast between any background and foreground elements, text is the key feature that needs to be legible. When a text colour fails a contrast check, consider using it on another element to achieve the same effect.&lt;/li&gt;
&lt;li&gt;Avoid Trends that Hinder Accessibility: While low-contrast, greyed-out text may be trendy, it often compromises readability. When torn between accessibility and aesthetics, there's likely a better solution.&lt;/li&gt;
&lt;li&gt;Keep Checking: Regularly test your designs for contrast at every step and for every design decision. Better to spend a little time doing this early than making changes during implementation. Use plugin tools in your design software such as the Figma plugin &lt;a href="https://www.figma.com/community/plugin/937465522075454889/color-contrast" rel="noopener noreferrer"&gt;Colour contrast&lt;/a&gt; or extensions to check within your browser.&lt;/li&gt;
&lt;/ul&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%2Fhmwsi6n7j2qsjgd0kcau.jpg" 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%2Fhmwsi6n7j2qsjgd0kcau.jpg" alt="Two side-by-side screenshots show contrast checks of two different buttons.&amp;lt;br&amp;gt;
  The left button is mid-tone blue with white text, the check shows a contrast&amp;lt;br&amp;gt;
  ratio of 5.04 and shows text passing WCAG AA and AAA. The right button is&amp;lt;br&amp;gt;
  light blue with white text, the check shows a contrast ratio of 2.43, with&amp;lt;br&amp;gt;
  text failing WCAG AA and AAA." width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example of a colour contrast Figma plugin inspecting colours of a button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Follow Your Eyes as Well as the Numbers
&lt;/h2&gt;

&lt;p&gt;While contrast ratios provide a numerical guideline, it's also important to consider the overall visual comfort, harmony, and emotive effect of your colours. Look at your designs and ensure your colours sit well together without causing visual strain or "vibrating" effects. Do they give off the right impression? Do they make you feel calm? Make you hungry? Research into colour psychology and utilising colour theory can help with this. Tools that implement complementary, analogous, or triadic colour schemes can be a good shortcut for creating aesthetically pleasing and accessible designs.&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%2Fnpj6dj3o5jle7dgkhq1f.jpg" 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%2Fnpj6dj3o5jle7dgkhq1f.jpg" alt="A photograph of the back cover of a book. The cover has an orange section&amp;lt;br&amp;gt;
  with purple text and a purple section with a quote in orange. The text is&amp;lt;br&amp;gt;
  barely legible due to the low colour contrast between the orange and&amp;lt;br&amp;gt;
  purple." width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even readers with perfect vision would struggle with the blurb of this book.&lt;/p&gt;

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

&lt;p&gt;With the upcoming EU law changes, ensuring colour contrast and accessibility in web design will become a legal requirement for many companies. This shift is ultimately beneficial for everyone, as prioritising accessibility creates more inclusive, user-friendly designs that enhance the experience for all users. The law change will necessitate audits and updates for many companies, but it also offers an opportunity to build trust and improve user satisfaction. Embrace these changes, and you'll be ahead of the curve in creating inclusive, effective digital products.&lt;/p&gt;

</description>
      <category>ux</category>
      <category>design</category>
      <category>a11y</category>
    </item>
    <item>
      <title>A RAG for Elixir</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Wed, 26 Jun 2024 13:50:49 +0000</pubDate>
      <link>https://forem.com/bitcrowd/a-rag-for-elixir-1da2</link>
      <guid>https://forem.com/bitcrowd/a-rag-for-elixir-1da2</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;This is the second part of a series of blog posts on using a RAG (Retrieval Augmented Generation) information system for your codebase. Together we explore how this can empower your development team. Check out the &lt;a href="https://dev.to/how-even-the-simplest-RAG-can-empower-your-team"&gt;first post&lt;/a&gt; for an introduction into the topic if you haven't already.&lt;/p&gt;

&lt;p&gt;In this episode we explore how we can adapt our RAG system for Ruby codebases from &lt;a href="https://bitcrowd.dev/how-even-the-simplest-RAG-can-empower-your-team"&gt;the first episode&lt;/a&gt; to read and understand Elixir code. We will take a look at &lt;a href="https://www.langchain.com/"&gt;LangChain&lt;/a&gt; and text &lt;em&gt;"splitting"&lt;/em&gt; and &lt;em&gt;"chunking"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let's dive right into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://github.com/bitcrowd/rag_time"&gt;RAG system&lt;/a&gt; was built with the idea to discover Ruby codebases. In order to have conversations about Elixir codebases as well, we need to make sure our LLM &lt;em&gt;"understands"&lt;/em&gt; Elixir code. This is where &lt;a href="https://www.langchain.com/"&gt;LangChain&lt;/a&gt; comes into play.&lt;/p&gt;

&lt;p&gt;LangChain is a toolkit around all things LLMs, including RAG. We use it to parse our documents or codebase and generate a vector database from it. In our &lt;a href="https://github.com/bitcrowd/rag_time"&gt;simple RAG system&lt;/a&gt;, we specify which file endings (.rb) and which programming language (Ruby) our documents have.&lt;/p&gt;

&lt;p&gt;The ingestion of programming source code into an LLM with LangChain was initially only supported for Python, C and a few others languages. Then &lt;a href="https://github.com/langchain-ai/langchain/issues/11229"&gt;this issue&lt;/a&gt; proposed the usage of a parser library like &lt;a href="https://tree-sitter.github.io/tree-sitter/"&gt;Tree-sitter&lt;/a&gt; to facilitate adding support for many more languages. The discussion is worth reading.&lt;/p&gt;

&lt;p&gt;Finally, &lt;a href="https://github.com/langchain-ai/langchain/pull/13318"&gt;this pull request&lt;/a&gt; introduced support for a lot more languages, including Ruby, based on this proposal. It was a school project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I am submitting this for a school project as part of a team of 5. Other team members are @LeilaChr, @maazh10, @Megabear137, @jelalalamy. This PR also has contributions from community members @Harrolee and @Mario928.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our plan is to use this as a starting point to enable basic parsing of Elixir source code with LangChain. With that, we should be able to have conversations with our RAG system about Elixir codebases as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting / Chunking text
&lt;/h2&gt;

&lt;p&gt;To read our Elixir codebase, the parser needs some rules on where to split the provided source code files at. Generally for RAG, when ingesting (reading in) a text file, PDF, etc. it will try to split it into chunks, ideally along it's semantic meaning. In text documents, the meanings are often grouped along:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;chapters&lt;/li&gt;
&lt;li&gt;paragraphs&lt;/li&gt;
&lt;li&gt;sentences&lt;/li&gt;
&lt;li&gt;words&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your embedding model has enough context capacity, you would try to split along chapters or paragraphs, because human readable text often groups meanings that way. If those are too big, you would try to break between sentences, and, as a last resort, words. One would generally try to avoid splitting inside words. Take for instance "sense" and "nonsense", which carry quite a different meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting / Chunking code
&lt;/h2&gt;

&lt;p&gt;Embedding code is a bit underdeveloped, but the strategy is to break the code into pieces by inserting new lines so that it looks a bit more like natural text, and then let the embedding model deal with the task of making sense (inferring meaning) from it. Interestingly, the models trained on that task do that surprisingly well.&lt;/p&gt;

&lt;p&gt;As said, LangChain has dedicated &lt;a href="https://python.langchain.com/v0.2/docs/integrations/document_loaders/source_code/"&gt;document loaders for source code&lt;/a&gt; and a &lt;a href="https://python.langchain.com/v0.2/docs/integrations/document_loaders/source_code/#adding-languages-using-tree-sitter-template"&gt;guide&lt;/a&gt; on how to add new ones based on Tree-sitter. So we went ahead and implemented a document loader and parser for Elixir source code in LangChain. It only covers the core basics of the language, but it was already enough for our proof-of-concept RAG application. With LangChain now supporting Elixir out of the box, people can use the parser in a variety of different scenarios and will come up with ways to improve it to fit more use cases. Our implementation is only the ground work. You can have a look at &lt;a href="https://github.com/langchain-ai/langchain/pull/22742"&gt;the PR&lt;/a&gt; if you're interested in what's necessary to add parsing support for another programming language in LangChain. Spoiler: not much if you can utilize Tree-sitter.&lt;/p&gt;

&lt;p&gt;The core of LangChain's programming language parsers based on Tree-sitter is their &lt;code&gt;CHUNK_QUERY&lt;/code&gt;. For our Elixir parser it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CHUNK_QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    [
        (call target: ((identifier) @_identifier
            (#any-of? @_identifier &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defmodule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defprotocol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defimpl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;))) @module
        (call target: ((identifier) @_identifier
            (#any-of? @_identifier &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;def&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defmacro&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defmacrop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;defp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;))) @function
        (unary_operator operator: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; operand: (call target: ((identifier) @_identifier
              (#any-of? @_identifier &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;moduledoc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;typedoc&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="s"&gt;doc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;)))) @comment
    ]
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are using Tree-sitter's own &lt;a href="https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries"&gt;tree query language&lt;/a&gt; here. Without diving into the details, our query makes sure to distinguish top level modules, functions and comments. The document loader will then take care of loading each chunk into a separate document and split the lines accordingly. The approach is the same for all programming languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test drive
&lt;/h2&gt;

&lt;p&gt;Let's take this for a spin in &lt;a href="https://github.com/bitcrowd/rag_time/"&gt;our RAG system scripts&lt;/a&gt; from &lt;a href="//./how-even-the-simplest-RAG-can-empower-your-team"&gt;episode one&lt;/a&gt; of this series.&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%2Fc6ztmed88vg80bpez9g6.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%2Fc6ztmed88vg80bpez9g6.png" alt="the scripts used are in the bitcrowd rag_time repo on GitHub" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Just as a refresher: the idea is to have a RAG system for your team's codebase using LLMs locally without exchanging any data with third parties like OpenAI and the like. It includes a conversational AI built with &lt;a href="https://docs.chainlit.io/get-started/overview"&gt;Chainlit&lt;/a&gt;, so that members of the team can &lt;em&gt;"chat"&lt;/em&gt; with the LLM about the codebase, for instance to get information about the domain or where to find things for the ticket they are working on.&lt;/p&gt;

&lt;p&gt;For testing purposes we will use our RAG system on a popular open source Elixir package, the &lt;a href="https://github.com/phoenixframework/phoenix"&gt;Phoenix Framework&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get the RAG ready
&lt;/h3&gt;

&lt;p&gt;First we need to get our local RAG system ready for operating on an Elixir codebase. It needs to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where is the code?&lt;/li&gt;
&lt;li&gt;Which programming language is it?&lt;/li&gt;
&lt;li&gt;Which suffixes have the source code files?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We provide this information via environment variables in a &lt;code&gt;.env&lt;/code&gt; file:&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;OLLAMA_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"llama3:8b"&lt;/span&gt;
&lt;span class="nv"&gt;CODEBASE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"./phoenix"&lt;/span&gt;
&lt;span class="nv"&gt;CODEBASE_LANGUAGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"elixir"&lt;/span&gt;
&lt;span class="nv"&gt;CODE_SUFFIXES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;".ex, .exs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We just cloned the current state of the Phoenix Git repository right next to our RAG code. We also keep using Meta's &lt;a href="https://llama.meta.com/llama3/"&gt;Llama3&lt;/a&gt; model, and instruct the document loader to look at Elixir files.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  ℹ️ Info
&lt;/h4&gt;

&lt;p&gt;At the time of testing &lt;a href="https://github.com/langchain-ai/langchain/pull/22742"&gt;our PR on LangChain&lt;/a&gt; was not released yet. So we were pointing to our fork's local code for the &lt;code&gt;langchain&lt;/code&gt;, &lt;code&gt;langchain-community&lt;/code&gt; and &lt;code&gt;langchain-text-splitter&lt;/code&gt; Python packages from the &lt;code&gt;requirements.txt&lt;/code&gt; file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the sake of simplicity we assume the project documentation to be in Markdown and hard-coded this information into our code ingestion logic.&lt;/p&gt;

&lt;p&gt;With that, we can set up our vector database for the Phoenix codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python ingest-code.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we start the chat bot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chainlit run main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Chatbot&lt;/strong&gt;:&lt;br&gt;&lt;br&gt;
Hi, Welcome to Granny RAG. Ask me anything about your code!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we are ready to have a conversation about the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ask questions
&lt;/h3&gt;

&lt;p&gt;Let’s take an &lt;a href="https://github.com/phoenixframework/phoenix/pull/5792"&gt;original PR&lt;/a&gt; from Phoenix to evaluate the quality of our solution, similar to what we did with Ruby in the previous post.&lt;/p&gt;

&lt;p&gt;The pull request fixes a small issue in the &lt;code&gt;recycle/1&lt;/code&gt; function of the &lt;code&gt;Phoenix.ConnTest&lt;/code&gt; module at &lt;code&gt;phoenix/lib/phoenix/test/conn_test.ex&lt;/code&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%2Fmhbka4jd74okwocxo6qn.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%2Fmhbka4jd74okwocxo6qn.png" alt="Screenshot of a pull request" width="800" height="681"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Currently, if you set &lt;code&gt;remote_ip&lt;/code&gt; during a test when building a conn, calling a dispatch call e.g get will run the conn through &lt;code&gt;recycle&lt;/code&gt; which will lose this information. This PR changes that so it is maintained similar to &lt;code&gt;host&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We reset our clone of the Phoenix repository (and our vector database) to the state right before the PR was merged and then ask the RAG system for help with the issue:&lt;/p&gt;

&lt;h4&gt;
  
  
  Understand the problem
&lt;/h4&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;We:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Please help me understand the following briefing:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Currently, if you set `remote_ip` during a test when building a `conn`,
calling &amp;gt; a dispatch call, e.g `get`, will run the `conn` through `recycle`
which will &amp;gt; lose this information. Instead, we want `the remote_ip`
maintained, similar to `host`.
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Can you see why &lt;code&gt;remote_ip&lt;/code&gt; is not preserved in test cases where &lt;code&gt;recycle&lt;/code&gt; is used?&lt;/p&gt;
&lt;/blockquote&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%2Ftn3klkccj6gpj83z8jbv.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%2Ftn3klkccj6gpj83z8jbv.png" alt="Screnshot of briefing question" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Chatbot:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The briefing explains that when running a test with Phoenix's Conn and Endpoint frameworks, the &lt;code&gt;remote_ip&lt;/code&gt; information is lost during the test. This happens because of the way Phoenix's &lt;code&gt;Conn.recycle/1&lt;/code&gt; function works.&lt;br&gt;
Here's what's happening:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In your tests, you create a new &lt;code&gt;conn&lt;/code&gt; with a specific IP address (e.g., &lt;code&gt;remote_ip = "192.168.0.100"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;You then dispatch an HTTP request to this connection using something like &lt;code&gt;get("/")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When you do this, Phoenix's Conn framework calls the &lt;code&gt;Conn.recycle/1&lt;/code&gt; function behind the scenes.&lt;/li&gt;
&lt;li&gt;This &lt;code&gt;recycle&lt;/code&gt; function is designed to clean up and reset the connection for subsequent requests.&lt;/li&gt;
&lt;li&gt;However, as part of this cleanup process, it loses the previously set &lt;code&gt;remote_ip&lt;/code&gt; information.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The reason why &lt;code&gt;host&lt;/code&gt; is preserved but not &lt;code&gt;remote_ip&lt;/code&gt; is that Phoenix's Conn framework has a special handling for the &lt;code&gt;host&lt;/code&gt; attribute. When you set the &lt;code&gt;host&lt;/code&gt; attribute, it gets stored in the connection's internal state. This means that when you recycle the connection, the &lt;code&gt;host&lt;/code&gt; information is still retained.&lt;/p&gt;

&lt;p&gt;On the other hand, &lt;code&gt;remote_ip&lt;/code&gt; is not preserved because it's not explicitly stored in the connection's internal state. Instead, it's just a temporary value set during the test execution.&lt;br&gt;
To maintain the &lt;code&gt;remote_ip&lt;/code&gt; information across tests, you can create a custom Conn module that overrides the default behavior of &lt;code&gt;Conn.recycle/1&lt;/code&gt;. This would involve storing the original &lt;code&gt;remote_ip&lt;/code&gt; value and restoring it after recycling the connection.&lt;/p&gt;

&lt;p&gt;In summary, &lt;code&gt;remote_ip&lt;/code&gt; is lost in test cases because Phoenix's &lt;code&gt;Conn.recycle/1&lt;/code&gt; function doesn't explicitly preserve this information. To keep &lt;code&gt;remote_ip&lt;/code&gt; intact, you'll need to create a custom Conn module that overrides the default behavior of &lt;code&gt;recycle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sources:&lt;br&gt;&lt;br&gt;
/Users/joshuabauer/code/rag_time/phoenix/test/phoenix/test/conn_test.exs,&lt;br&gt;&lt;br&gt;
[…]&lt;/p&gt;
&lt;/blockquote&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%2F98if0pkhle3ac3zcl2ma.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%2F98if0pkhle3ac3zcl2ma.png" alt="Screnshot of briefing answer" width="800" height="1136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Implement a solution
&lt;/h4&gt;

&lt;p&gt;This would give us a hint on how to solve the issue in our app if we were &lt;em&gt;"using"&lt;/em&gt; the Phoenix framework. But let's say we want to fix the issue &lt;em&gt;"for good"&lt;/em&gt; in the framework itself. We now know the &lt;code&gt;Conn.recycle/1&lt;/code&gt; function is what we're interested in. So we look at the listed sources, search for the &lt;code&gt;recycle/1&lt;/code&gt; function in the code and find &lt;code&gt;ConnTest/recycle/1&lt;/code&gt; as the test-conn equivalent to &lt;code&gt;Conn.recycle/1&lt;/code&gt;. This is pretty much already be the "custom Conn module" the RAG was referring to and it's just used for tests. And after all, we only want to change the behavior for tests. So let's ask the RAG what to do with it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;We:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Given the following code from &lt;code&gt;phoenix/lib/phoenix/test/conn_test.ex&lt;/code&gt;, what would I need to change to persist &lt;code&gt;conn.remote_ip&lt;/code&gt; in the same way as &lt;code&gt;conn.host&lt;/code&gt;?&lt;/p&gt;


&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;recycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="sx"&gt;~w(accept accept-language authorization)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;build_conn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recycle_cookies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_peer_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_peer_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;copy_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;p&gt;&lt;strong&gt;Chatbot:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
A Phoenix ConnTest question!&lt;/p&gt;

&lt;p&gt;To persist &lt;code&gt;conn.remote_ip&lt;/code&gt; in the same way as &lt;code&gt;conn.host&lt;/code&gt;, you would need to add a line to the &lt;code&gt;recycle/2&lt;/code&gt; function to copy the remote IP address.&lt;/p&gt;

&lt;p&gt;Here's how:&lt;/p&gt;


&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;recycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;\\&lt;/span&gt; &lt;span class="sx"&gt;~w(accept accept-language authorization)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;build_conn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:remote_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remote_ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Add this line&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recycle_cookies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Test&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_peer_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Plug&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="no"&gt;Conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_peer_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;copy_headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req_headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;p&gt;Now, when you recycle a connection, both &lt;code&gt;conn.host&lt;/code&gt; and &lt;code&gt;conn.remote_ip&lt;/code&gt; will be preserved.&lt;/p&gt;

&lt;p&gt;Sources:&lt;br&gt;&lt;br&gt;
[…]&lt;/p&gt;
&lt;/blockquote&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%2Fd3pdem8f2ylgzc8xznla.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%2Fd3pdem8f2ylgzc8xznla.png" alt="Screnshot of dialogue about implementation" width="800" height="1136"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Compare solution
&lt;/h4&gt;

&lt;p&gt;Looking at the PR's &lt;a href="https://github.com/phoenixframework/phoenix/pull/5792/files"&gt;file changes&lt;/a&gt;, this is exactly what the person came up with:&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%2F8259c3h4gdr6mf76l1hh.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%2F8259c3h4gdr6mf76l1hh.png" alt="screenshot of the PR file changes on GitHub" width="800" height="596"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the conversation with our Elixir RAG was quite helpful for guiding us through the code and finding an adequate solution to the problem.&lt;/p&gt;

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

&lt;p&gt;It is really easy! Just &lt;a href="https://github.com/bitcrowd/rag_time"&gt;clone our repo&lt;/a&gt;, follow the README and tell the script where to find your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CODEBASE_PATH="./path-to-my-elixir-codebase"
CODEBASE_LANGUAGE="elixir"
CODE_SUFFIXES=".ex, .exs"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We kept the scripts basic, so that they are easy to understand and extend. Depending on your codebase, the results might not always be perfect, but often surprisingly good.&lt;/p&gt;

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

&lt;p&gt;In this post we saw how we can extend a simple off-the-shelf system to better fit the needs of our dev team. We enabled our RAG system to read and understand Elixir code! Text splitting and chunking is just one possible example of where to start when it comes to adjusting a RAG system for your specific needs. What we got is already quite useful, but it's definitely still lacking precision.&lt;/p&gt;

&lt;p&gt;We will explore possibilities for further improvements and fine tuning in the next episodes of this blog post series.&lt;/p&gt;

&lt;p&gt;Or, if you canʼt wait, give the team at bitcrowd a shout via &lt;a href="mailto:granny-rag@bitcrowd.net"&gt;granny-rag@bitcrowd.net&lt;/a&gt; or &lt;a href="https://calendly.com/calendly-6kjr/30min"&gt;book a consulting call here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>rag</category>
      <category>ai</category>
      <category>elixir</category>
    </item>
    <item>
      <title>How even the simplest RAG can empower your team</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Wed, 26 Jun 2024 13:34:01 +0000</pubDate>
      <link>https://forem.com/bitcrowd/how-even-the-simplest-rag-can-empower-your-team-15ai</link>
      <guid>https://forem.com/bitcrowd/how-even-the-simplest-rag-can-empower-your-team-15ai</guid>
      <description>&lt;h2&gt;
  
  
  Abstract
&lt;/h2&gt;

&lt;p&gt;This is the first part of a series of blog posts on using a RAG (Retrieval Augmented Generation) information system for coding. Find out how this can empower your development team.&lt;/p&gt;

&lt;p&gt;In this episode, we will discuss at a very simple &lt;a href="https://www.datacamp.com/blog/what-is-retrieval-augmented-generation-rag"&gt;RAG&lt;/a&gt; system for Ruby made with &lt;a href="https://python.langchain.com/v0.2/docs/tutorials/"&gt;LangChain&lt;/a&gt;, &lt;a href="https://jina.ai/news/elevate-your-code-search-with-new-jina-code-embeddings"&gt;JinaAI embeddings&lt;/a&gt; and a very light local LLM served via &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you donʼt know what any of this means, this blog post is for you. You will see how to benefit from the technology without uploading/donating your codebase to external providers like &lt;a href="https://openai.com/"&gt;OpenAI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It is an introduction, the coding follows in the next episodes.&lt;/p&gt;

&lt;p&gt;Naming is hard, and I wanted something that goes against the flashy-ai-y. So letʼs call this a &lt;strong&gt;&lt;em&gt;Granny RAG.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The next parts will then move on to the following topics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;examine different embedding models&lt;/li&gt;
&lt;li&gt;splitting and chunking strategies&lt;/li&gt;
&lt;li&gt;RAG for and with &lt;a href="https://elixir-lang.org/"&gt;Elixir&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Letʼs start.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Use Case
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Imagine you are developer and you just started a new job. The code base looks well maintained, the team is nice. Could be &lt;a href="https://bitcrowd.net/de/careers"&gt;bitcrowd&lt;/a&gt;. You grab your first ticket, and it looks like this:&lt;/em&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%2Fcb3mqli6ae7nbj12v1or.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%2Fcb3mqli6ae7nbj12v1or.png" alt="A ticket with a missing description" width="501" height="277"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Uff. No description. Normally, you would reach out to your onboarding-buddy and ask for advise. This ticket is clearly written with a developers who knows the application in mind.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;But ... you could also quickly investigate. Your team does have a &lt;strong&gt;Granny&lt;/strong&gt; after all!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You go to your local granny. Granny RAG is in fact just some light off-the-shelf application wrapper around a simple RAG system, but you are used to call her Granny RAG, because she is patient, kind and &lt;strong&gt;knows all the stories&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Letʼs ask her:&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hy Granny&lt;/strong&gt;, I need to show the end date of the subscriptions in the “subscription list”. What is a subscription in the application?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granny&lt;/strong&gt;: To display the end dates of subscriptions in the list of subscriptions, you can modify the COLLECTION_ATTRIBUTES constant in your SubscriptionDashboard class to include the active_until field. Hereʼs an updated version of the SubscriptionDashboard class with the modified COLLECTION_ATTRIBUTES:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class SubscriptionDashboard &amp;lt; ApplicationDashboard
 ATTRIBUTE_TYPES = {
   course: Field::BelongsTo.with_options(order: :name),
   user: Field::BelongsTo.with_options(order: :name),
   user_id: Field::Number,
   id: Field::String,
   active_until: Field::DateTime,
   deactivated_at: Field::DateTime,
   active?: Field::Boolean,
   product_type: EnumField,
   created_at: Field::DateTime,
   updated_at: Field::DateTime
 }.freeze

 COLLECTION_ATTRIBUTES = %i[
   course
   user
   id
   active?
   product_type
   active_until
 ].freeze
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wow, this is magic!
&lt;/h2&gt;

&lt;p&gt;Without knowing the application too well, you came from a non-workable ticket to a plan of action. You can now chose to update the ticket description, or create a PR and use that for the discussion.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  ℹ️ Note
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This is a real life ticket from a client.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This ticket came from the work at our client &lt;a href="https://medgurus.de"&gt;MedGurus&lt;/a&gt;. When I tried out my Granny RAG, I browsed through the tickets in progress and found this one. I compared the solution Granny RAG proposed with the PR that came out of this ticket. I was actually thrilled how well this works.&lt;/p&gt;

&lt;p&gt;My conversation with Granny RAG was actually a a bit longer. &lt;a href="https://bitcrowd.dev/how-even-the-simplest-RAG-can-empower-your-team/#wow-this-is-magic"&gt;Here&lt;/a&gt; is the full conversation with Granny RAG.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  How does it all work?
&lt;/h2&gt;

&lt;p&gt;Granny RAG is a RAG system. That is short for Retrieval Augmented Generation. If you are looking for a quick intro, &lt;a href="https://www.youtube.com/watch?v=T-D1OfcDW1M&amp;amp;t=10s"&gt;here is the nice video by Marina Danilevsky&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In essence, RAG improves the quality of LLM responses by enriching user prompts with relevant contextual information. It retrieves this information from an efficiently searchable index of of your entire project, generated with the help of an &lt;a href="https://huggingface.co/blog/getting-started-with-embeddings"&gt;&lt;em&gt;embedding model&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedding models
&lt;/h3&gt;

&lt;p&gt;Itʼs not easy to say something simple about the embedding process without being incorrect. Embedding models are models that generate a representation of the “meaning” sequence of text. This “meaning” is represented as a vector called “embedding”. It is a long array of numbers that represent semantic meaning within the given context.&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%2Ftsakc2pefzvm5bdinec7.jpg" 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%2Ftsakc2pefzvm5bdinec7.jpg" alt="Embeddings represent meaning as multidimentional vectors" width="800" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tokens with a similar &lt;strong&gt;meaning&lt;/strong&gt; in the source document get embedding vectors “close to each other” by some distance measurement.&lt;/p&gt;

&lt;p&gt;A suitable model will place expressions with &lt;em&gt;similar meaning&lt;/em&gt; in &lt;em&gt;similar spaces&lt;/em&gt; of its &lt;em&gt;vector space&lt;/em&gt;. So &lt;code&gt;subscription&lt;/code&gt; will be next to &lt;code&gt;activation&lt;/code&gt; and &lt;code&gt;active_until&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can think of the process as hashing with hashing function that understands the input.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retrieval
&lt;/h3&gt;

&lt;p&gt;Instead, when the user asks a question, we throw it into the same embedding function to get an index for it. With that, we do a lookup what sequences of text occupy a similar space in the memory.&lt;/p&gt;

&lt;p&gt;There are multiple strategies for this similarity criteria. We will explore similarity in more depth in the second post of this series. For now, letʼs assume we found entries “close” to the index we got for the search term.&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%2F6mzao92zx5nzmmyf7yhh.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%2F6mzao92zx5nzmmyf7yhh.png" alt="a picture showing an embedding in the vector space" width="800" height="508"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each of those entries carries a piece of text and some metadata. The metadata tells us more about the source, e.g. which file it came from. Until now, we have build a more intelligent search function. It finds &lt;code&gt;active_until&lt;/code&gt; even if you searched for &lt;code&gt;end date&lt;/code&gt;. Something, a classic fulltext index would not find.&lt;/p&gt;

&lt;p&gt;In an “old fashioned” information system, we would output those magical pieces of text and leave it to the reader to go through them, understand their meaning and evaluate their relevance.&lt;/p&gt;

&lt;p&gt;“But wait”, you say, “are there not these new cool kids on the block, &lt;strong&gt;The LLMʼs&lt;/strong&gt;™, that are brilliant at exactly that?”. You are right, this is exactly what RAG systems do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Attention: We will be simplifying heavily.&lt;/em&gt;&lt;/strong&gt; If you would like to get a l ightweight intro head over to this &lt;a href="https://huggingface.co/learn/nlp-course/chapter1/1"&gt;huggingface course&lt;/a&gt;, or this series of videos from &lt;a href="https://www.youtube.com/playlist?list=PLZHQObOWTQDNU6R1_67000Dx_ZCJB-3pi"&gt;three blue one brown&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It boils down to this: When LLMs generate, they find the next word, or gaps in a text. They take this a step at a time, a bit like friends finishing each otherʼs sentences.&lt;/p&gt;

&lt;p&gt;Then, they look at the text created, including the new word, and compile the next word, and the next. Put differently, they try to find the piece of text or the character that is most likely to make sense in the previously generated context.&lt;/p&gt;

&lt;p&gt;Here is an example for a prompt that uses RAG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an assistant for question-answering tasks. Use the following pieces of
retrieved context to answer the question. If you donʼt know the answer, just
say that you donʼt know.
Use three sentences maximum and keep the answer concise. # (1)
--
Question: “What would I need to change to show the active_until date in the list
of subscriptions?” # (2)

Context: {context} # &amp;lt;- The RAG magic happens here

Answer: # (3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;h4&gt;
  
  
  ℹ️ Info
&lt;/h4&gt;

&lt;p&gt;A system prompt tells the LLM what is expected from it (1), then a question is specifying the task (2) and the “please fill in your answer here”-part (3) is what LLMs are used to work with.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LLMs do so, again, based on vector representations. Starting from a seed, often the system prompt, and the userʼs instructions.&lt;/p&gt;

&lt;p&gt;The idea of RAG is that if you include facts that you researched into your prompt, the context for the generation is narrowed down significantly compared to a prompt that does not include those facts. Retrieval Augmented Generation is an effective countermeasure against hallucinations. It does not stop them, but makes them less likely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;h4&gt;
  
  
  💡 Tip
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Outline the Important Findings in your follow up&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;All LLM based systems hallucinate at some point. RAG helps to avoid that, but as you can see in &lt;strong&gt;5. Follow Up&lt;/strong&gt;, even Retrieval Based Systems stray from the truth at times. You can detect that because the information in &lt;strong&gt;5. Follow Up&lt;/strong&gt; does not align with the previous answers.&lt;/p&gt;

&lt;p&gt;If this happens, &lt;strong&gt;it helps to outline the previous facts in the next prompt&lt;/strong&gt;, as I did in &lt;strong&gt;6. Follow Up&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;...&lt;/p&gt;

&lt;p&gt;“You said before that the &lt;code&gt;COLLECTION_ATTRIBUTES&lt;/code&gt; are responsible for the list of &lt;code&gt;Subscriptions&lt;/code&gt; (...)”&lt;/p&gt;

&lt;p&gt;...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Mentioning previous findings in the new prompt amplifies them in the context.&lt;/strong&gt; This steers the conversation in the direction you like and helps helps the LLM to let go of the hallucinations.&lt;/p&gt;


&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Code
&lt;/h2&gt;

&lt;p&gt;You can find the code &lt;a href="https://github.com/bitcrowd/rag_time"&gt;on bitcrowdʼs GitHub&lt;/a&gt;, which is probably easier to read than going through the inline version here.&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%2Fddwlxn49hvt0umo226sw.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%2Fddwlxn49hvt0umo226sw.png" alt="Clone the code from the bitcrowd rag_time repo" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will use &lt;a href="https://python.langchain.com/v0.2/docs/tutorials/"&gt;LangChain&lt;/a&gt;, which is a Python library. There are also implementations of it in &lt;a href="https://js.langchain.com/v0.2/docs/tutorials/"&gt;JavaScript&lt;/a&gt; and &lt;a href="https://github.com/brainlid/langchain"&gt;Elixir&lt;/a&gt;. We use Python to start with, because it is the common language for machine learning, so prototyping is easy. Once we know what we want, we will explore the Elixir way. So please make sure you have Python installed.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://jina.ai/news/elevate-your-code-search-with-new-jina-code-embeddings"&gt;JinaAI embeddings&lt;/a&gt;, which is a very capable model that can be used locally under the Apache 2 licence.&lt;/p&gt;

&lt;p&gt;Finally, you need &lt;a href="https://ollama.com/"&gt;Ollama&lt;/a&gt;, or any other tool that letʼs you run a model and expose it to a web endpoint. In the example, we use Meta's &lt;a href="https://llama.meta.com/llama3/"&gt;Llama3&lt;/a&gt; model. Models like CodeLlama:7b-instruct also work. Feel free to change the &lt;code&gt;.env&lt;/code&gt; file and experiment with different models.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test Drive
&lt;/h2&gt;

&lt;p&gt;You need some Ruby library that we can try out here. Letʼs go for &lt;a href="https://github.com/discourse/discourse"&gt;Discourse&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Weʼre interested in the logic Discourse uses prevent users from re-using an old, expired password and which files in the code are relevant for this. So we ask it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How does Discourse keep track of which passwords a user already used? So that when changing their password, a user does not use the same, expired password again.&lt;/p&gt;
&lt;/blockquote&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%2Fitf2i9znzyxx49hmd1xf.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%2Fitf2i9znzyxx49hmd1xf.png" alt="A screenshot of the chat UI when entering the question" width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Granny RAG&lt;/strong&gt; answers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;According to the context you provided, Discourse keeps track of which passwords a user already used by storing the old password hash and algorithm in the &lt;code&gt;user_passwords&lt;/code&gt; table. This is done through the following mechanisms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;user_passwords&lt;/code&gt; table stores the userʼs previous passwords along with their corresponding salts and algorithms.&lt;/li&gt;
&lt;li&gt;When a user changes their password, Discourse generates a new password hash using the new password, salt, and algorithm.&lt;/li&gt;
&lt;li&gt;Discourse then checks if the new password hash is already present in the &lt;code&gt;user_passwords&lt;/code&gt; table for that user. If it is, it means the user has used this password before, and therefore, itʼs considered expired.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By storing the old password hashes and algorithms in the database, Discourse can easily verify whether a user has used a particular password before and prevent them from using an expired password again.&lt;/p&gt;

&lt;p&gt;This approach ensures that users cannot reuse the same password twice, even if they change their password multiple times.&lt;br&gt;
…&lt;/p&gt;
&lt;/blockquote&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%2Fes84i77xtbnnit7dvld0.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%2Fes84i77xtbnnit7dvld0.png" alt="A screenshot of the chat UI conversation" width="800" height="1128"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Without knowing all the details of Discourseʼ codebase, this seems a perfectly valid answer. It gives us a nice summary of the feature an idea on where to go looking for the implementation in Discourse otherwise overwhelmingly big codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;For our InfoServer Granny RAG we created a sophisticated search index for our codebase. This index allows us to infer meaning. We can now search for an answer by infering the meaning of the question and retrieve relavant information by looking what is close in the vector space.&lt;/p&gt;

&lt;p&gt;We fed this information to an LLM to understand and prioritize the sources. We made the LLM create answers to our questions, based on its training data and the information retrieved.&lt;/p&gt;

&lt;p&gt;So with a few, off-the-shelf, open source components, we manage to get expert knowledge about our code base, and get it delivered to us by our friendly LLM companion, hosted on our own premises.&lt;/p&gt;

&lt;h2&gt;
  
  
  How is this different from Copilot, Duo and friends?
&lt;/h2&gt;

&lt;p&gt;If you ask Granny RAG a question, it can draw &lt;strong&gt;information from the whole codebase&lt;/strong&gt;. It is able to incorporate &lt;strong&gt;all kinds of documents&lt;/strong&gt;, and can be extended to incorporate &lt;strong&gt;additional data-sources&lt;/strong&gt;. Granny RAG operates on &lt;strong&gt;resource effective, local LLMs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No data needs to leaves your control.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The scripts that ingest and embed your data and code can be specific to your needs&lt;/strong&gt; - as is your codebase. That way, you can even specify &lt;strong&gt;what should, and what should not, find its way into your RAG knowledge base&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copilot and GitLab Duo have a much narrower angle of vision.&lt;/strong&gt; Their context is primarily the opened files of the editor, or the PR. That means, once you know where to look, they can be helpful. Both to you and their creators, which can (and probably will) use &lt;em&gt;some&lt;/em&gt; data to improve their models. Even if, per contract, your data and code should not be shared with GitLab or Microsoft, you lost all control once your data leaves the premises.&lt;/p&gt;

&lt;p&gt;If you set theses concerns aside, you still have &lt;strong&gt;little control about what makes its way into the LLMs that are hosted on remote servers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here again, &lt;strong&gt;Granny RAG is different&lt;/strong&gt;. You can collect data from usage and reactions, and you can use that data to &lt;strong&gt;train both, LLM and embedding model, on your data and needs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That way, new arrivals in your dev team get an assistant that is steadily improving. &lt;strong&gt;Granny RAG can integrate into a &lt;a href="https://slack.com/"&gt;Slack&lt;/a&gt; channel to provide a first opinion&lt;/strong&gt;, and take feedback from the more seasoned developers to improve.&lt;/p&gt;

&lt;p&gt;All in all, Granny RAG is a concept that can (and should) be adopted to your use-case and needs. Itʼs not a subscription you buy, but a technique your team learns to master. You invest in consulting or learning time, and you get control and excellent knowledge about the core or your business logic.&lt;/p&gt;

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

&lt;p&gt;It is really easy! Just &lt;a href="https://github.com/bitcrowd/rag_time"&gt;clone our repo&lt;/a&gt;, follow the README and tell the script where to find your codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CODEBASE_PATH="./path-to-my-codebase"
CODEBASE_LANGUAGE="ruby"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We kept the scripts basic, so that they are easy to understand and extend. Depending on your codebase, the results might not always be perfect, but often surprisingly good.&lt;/p&gt;

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

&lt;p&gt;In this introductory post, we saw what a little off-the-shelf system can achieve. Itʼs already impressive, and it only uses local models, namely Llama3 and JinaAI Code.&lt;/p&gt;

&lt;p&gt;You will find that this off-the-shelf solution is lacking precision in some use cases. To improve this, we will explore how changes in the parsing, chunking and embedding strategies will change performance in the next episodes of this blog post series.&lt;/p&gt;

&lt;p&gt;Or, if you canʼt wait, give the team at bitcrowd a shout via &lt;a href="mailto:granny-rag@bitcrowd.net"&gt;granny-rag@bitcrowd.net&lt;/a&gt; or &lt;a href="https://calendly.com/calendly-6kjr/30min"&gt;book a consulting call here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>rag</category>
      <category>ai</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Idempotent seeds in Elixir</title>
      <dc:creator>bitcrowd</dc:creator>
      <pubDate>Thu, 14 Mar 2024 09:36:37 +0000</pubDate>
      <link>https://forem.com/bitcrowd/idempotent-seeds-in-elixir-4hd3</link>
      <guid>https://forem.com/bitcrowd/idempotent-seeds-in-elixir-4hd3</guid>
      <description>&lt;p&gt;A standard Phoenix app contains a &lt;a href="https://github.com/phoenixframework/phoenix/blob/3c5993e459fef0d2e66259722e27975bd651979d/installer/templates/phx_ecto/seeds.exs"&gt;&lt;code&gt;priv/repo/seeds.exs&lt;/code&gt;&lt;/a&gt; script file, which populates a database when it is run, so that developers can work with a conveniently prepared environment.&lt;/p&gt;

&lt;p&gt;At Bitcrowd, we like our seeds to be idempotent. In practice, this means that running &lt;code&gt;mix run priv/repo/seeds.exs&lt;/code&gt; multiple times will not create more rows each time, but rather upsert the existing data. While it is always possible to drop the local database with &lt;code&gt;mix ecto.reset&lt;/code&gt;, we might want to keep the current state of our development database.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Upsert all the things!
&lt;/h2&gt;

&lt;p&gt;Our strategy here is to use &lt;a href="https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert/2"&gt;&lt;code&gt;insert!&lt;/code&gt;&lt;/a&gt; with a &lt;code&gt;conflict_target&lt;/code&gt; on the &lt;code&gt;:id&lt;/code&gt;. Let's take a classic blog app with &lt;code&gt;Post&lt;/code&gt; and &lt;code&gt;Comment&lt;/code&gt; schemas for the sake of the example. In our &lt;code&gt;seeds.exs&lt;/code&gt; file we add this little helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;insert_idempotently&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on_conflict:&lt;/span&gt; &lt;span class="ss"&gt;:replace_all&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;conflict_target:&lt;/span&gt; &lt;span class="ss"&gt;:id&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;And instead of inserting a seed record like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="no"&gt;Repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insert!&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Hello World!"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We wrap our insert in the helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"64af9d13-0f60-45fc-971f-07e6b490c059"&lt;/span&gt;
&lt;span class="n"&gt;insert_idempotently&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="nv"&gt;@id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Hello World!"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nice 🎉&lt;/p&gt;

&lt;p&gt;Next time we run the seeds, we will not get a second &lt;code&gt;Post&lt;/code&gt; row in the database if a post with this &lt;code&gt;:id&lt;/code&gt; already exists. This keeps our development environment neat and clean. &lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency for &lt;code&gt;has_many&lt;/code&gt; associations
&lt;/h2&gt;

&lt;p&gt;While the previous example is fairly simple, hardcoding UUIDs has its limitations when seeding &lt;code&gt;has_many&lt;/code&gt; associations. For example, let's say we want to insert 50 &lt;code&gt;Comments&lt;/code&gt; associated to the &lt;code&gt;Post&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"64af9d13-0f60-45fc-971f-07e6b490c059"&lt;/span&gt;
&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;insert_idempotently&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="nv"&gt;@id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Hello World!"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
  &lt;span class="n"&gt;insert_idempotently&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="sx"&gt;???,&lt;/span&gt; 
    &lt;span class="ss"&gt;message:&lt;/span&gt; &lt;span class="s2"&gt;"Comment &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&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="ss"&gt;post_id:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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;Of course one could add 50 lines of hardcoded UUIDs… Or could we generate deterministic UUIDs from the &lt;code&gt;index&lt;/code&gt; value? Yes we can ⚡️!&lt;/p&gt;

&lt;h3&gt;
  
  
  Deterministic UUID v4 from a string
&lt;/h3&gt;

&lt;p&gt;Our deterministic UUID generator should take a string as an argument, and always return the same UUID for the same argument. We first need to hash our string, and then to extract the number of bits that we need. UUIDs have a consistent structure: &lt;code&gt;"64af9d13-0f60-45fc-971f-07e6b490c059"&lt;/code&gt;: one group of 8 characters, then three groups of 4, and finally a group of 12, all separated by a &lt;code&gt;-&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;deterministic_uuid4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Hash the string and extract the 128 bits,&lt;/span&gt;
  &lt;span class="c1"&gt;# and match on the length of our characters group&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;_rest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="ss"&gt;:crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Override some bits (necessary to create valid UUID v4)&lt;/span&gt;
  &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x0FFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mh"&gt;0x4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x0FFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mh"&gt;0x8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# Glue all of the chunks together and turn it into a string&lt;/span&gt;
  &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map_join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:binary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode_unsigned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;&amp;amp;1&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;Let's see it in action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iex&lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; MyApp.Seeds.deterministic_uuid4&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"foo"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;"2C26B46B-68FF-468F-899B-453C1D304134"&lt;/span&gt;

iex&lt;span class="o"&gt;(&lt;/span&gt;2&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; MyApp.Seeds.deterministic_uuid4&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"foo"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;"2C26B46B-68FF-468F-899B-453C1D304134"&lt;/span&gt;

iex&lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; MyApp.Seeds.deterministic_uuid4&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"bar"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;"FCDE2B2E-DBA5-4BF4-8860-1FB721FE9B5C"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, let's validate that our generated UUID is valid with the &lt;a href="https://hexdocs.pm/uuid/readme.html"&gt;uuid&lt;/a&gt; utility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iex&lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&amp;gt;&lt;/span&gt; UUID.info&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"2C26B46B-68FF-468F-899B-453C1D304134"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;:ok,
 &lt;span class="o"&gt;[&lt;/span&gt;
   uuid: &lt;span class="s2"&gt;"2C26B46B-68FF-468F-899B-453C1D304134"&lt;/span&gt;,
   binary: &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;44&lt;/span&gt;&lt;span class="sh"&gt;, 38, 180, 107, 104, 255, 70, 143, 137, 155, 69, 60, 29, 48, 65,
     52&amp;gt;&amp;gt;,
   type: :default,
   version: 4,
   variant: :rfc4122
 ]}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's rewrite our seeds to make use of our brand new function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="nv"&gt;@id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"64af9d13-0f60-45fc-971f-07e6b490c059"&lt;/span&gt;
&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;insert_idempotently&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="nv"&gt;@id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;title:&lt;/span&gt; &lt;span class="s2"&gt;"Hello World!"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
  &lt;span class="n"&gt;insert_idempotently&lt;/span&gt;&lt;span class="p"&gt;(%&lt;/span&gt;&lt;span class="no"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;deterministic_uuid4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"comment-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&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="ss"&gt;message:&lt;/span&gt; &lt;span class="s2"&gt;"Comment &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;index&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="ss"&gt;post_id:&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&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;Amazing! We won't get 50 new rows of &lt;code&gt;Comment&lt;/code&gt; each time we run the seeds script. Our development database is clean and we made our developers happy ☕️.&lt;/p&gt;

&lt;h3&gt;
  
  
  UUID Version-5
&lt;/h3&gt;

&lt;p&gt;To ruin the party, deterministic UUID generation is exactly what UUID v5 is designed for. And since &lt;a href="https://github.com/elixir-ecto/ecto/issues/2147"&gt;Ecto does not validate UUIDs against their specs&lt;/a&gt;, you might as well use &lt;a href="https://hexdocs.pm/uuid/readme.html"&gt;uuid&lt;/a&gt; again and do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="n"&gt;iex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"foo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;"aa752cea-8222-5bc8-acd9-555b090c0ccb"&lt;/span&gt;

&lt;span class="n"&gt;iex&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uuid5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"foo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;"aa752cea-8222-5bc8-acd9-555b090c0ccb"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what's the fun in that 🤷‍♀️.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.uuidtools.com/uuid-versions-explained"&gt;UUID Versions Explained&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.1"&gt;rfc4122&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sohamkamani.com/uuid-versions-explained/"&gt;More about how UUIDs are structured&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;If you enjoyed reading this, you might be interested in working with Elixir at bitcrowd. Check our &lt;a href="https://bitcrowd.net/jobs/backend-developer"&gt;job offerings&lt;/a&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>elixir</category>
      <category>seeds</category>
      <category>phoenix</category>
    </item>
    <item>
      <title>Git, GitHub and your work email</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Mon, 09 Aug 2021 16:35:55 +0000</pubDate>
      <link>https://forem.com/bitcrowd/git-github-and-your-work-email-365i</link>
      <guid>https://forem.com/bitcrowd/git-github-and-your-work-email-365i</guid>
      <description>&lt;p&gt;A possible setup for separating private and work-related contributions on &lt;a href="https://github.com"&gt;GitHub&lt;/a&gt;: associate commits with your &lt;em&gt;"private"&lt;/em&gt; vs. your "&lt;em&gt;work"&lt;/em&gt; email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com"&gt;GitHub&lt;/a&gt; being such a convenient place to collaborate on software, it's likely that both, work and &lt;em&gt;"leassure coding"&lt;/em&gt; take place on the same platform. Do you need two separate accounts for both activities? Eventually yes… But to spare the hassle of switching accounts back and forth and remembering to actually do that, a lot of people - including myself - prefer to use a single account (as long as this is legally aligned with your work place). However, aside from keeping that yin-yang of a work-life-balance, fully blending in your work-related coding into your private ramblings on GitHub and vice versa feels wrong from multiple angles, mainly the legal one. In which role did you contribute this piece of code, who &lt;em&gt;"owns"&lt;/em&gt; this contribution now? You, your employee, a client?&lt;/p&gt;

&lt;p&gt;So a bit of separation can come in handy. But how to get there without juggling accounts? Say hi to your old friend from the 80s: &lt;strong&gt;email&lt;/strong&gt; 📧&lt;/p&gt;

&lt;p&gt;Different to what one would expect on first glance, GitHub does not directly associate contributions with accounts. It's rather just &lt;em&gt;"connecting"&lt;/em&gt; the contribution to the account that happens to own the email address associated with a Git commit &lt;sup&gt;¹&lt;/sup&gt;. Listen to the &lt;a href="https://www.bikeshed.fm/35"&gt;GitHub Ate My Résumé&lt;/a&gt; episode of &lt;a href="https://www.bikeshed.fm"&gt;The Bike Shed&lt;/a&gt; podcast for some wild situations this can bring…&lt;/p&gt;

&lt;p&gt;It's probably save to assume most people have a dedicated email address for work. Using your private email address for work-related communication uncomfortably stresses the yin-yang and should be avoided. And since we're already there, why not apply the same email hygiene to your commits? Use your work email address for work contributions and your &lt;code&gt;riotcat1991@smthng&lt;/code&gt; address for your private coding adventures.&lt;/p&gt;

&lt;p&gt;Here are some tips how I set this up for myself. Disclaimer: people are different. While this works fine for me, other solutions may eventually work better for you - find what feels comfy 🧘.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git Config ⚙️
&lt;/h2&gt;

&lt;p&gt;The setup mainly lives in the &lt;a href="https://git-scm.com/docs/git-config"&gt;gitconfig&lt;/a&gt; files in my home directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/
├── .gitconfig
├── .gitconfig_personal
└── .gitconfig_work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gitconfig
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;.gitconfig&lt;/code&gt;, set a name via &lt;code&gt;user.name&lt;/code&gt; and deliberately &lt;strong&gt;not configure&lt;/strong&gt; any email address in &lt;code&gt;user.email&lt;/code&gt;. I combine this with setting &lt;a href="https://git-scm.com/docs/git-config#Documentation/git-config.txt-useruseConfigOnly"&gt;&lt;code&gt;user.useConfigOnly&lt;/code&gt;&lt;/a&gt; to &lt;code&gt;true&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Instruct Git to avoid trying to guess defaults for &lt;code&gt;user.email&lt;/code&gt; and &lt;code&gt;user.name&lt;/code&gt;, and instead retrieve the values only from the configuration&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recent versions of Git don't allow to commit without having a name and email address configured for the repository. The email address is an integral part of a commit. By not configuring one globally and instructing Git to not magically figure one out itself, I can avoid accidentally committing with the &lt;em&gt;"wrong"&lt;/em&gt; email address.&lt;/p&gt;

&lt;p&gt;With this configuration, I now have to manually set &lt;code&gt;user.email&lt;/code&gt; for every repository. For convenience, I have two alias set up for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;alias&lt;/span&gt;]
  &lt;span class="n"&gt;personal&lt;/span&gt; = &lt;span class="s2"&gt;"config user.email 'me@private.internet'"&lt;/span&gt;
  &lt;span class="n"&gt;work&lt;/span&gt; = &lt;span class="s2"&gt;"config user.email 'me@work.internet'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a Git repository, running &lt;code&gt;git work&lt;/code&gt; configures the email to be used for commits in that repository to my work address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Directories
&lt;/h3&gt;

&lt;p&gt;In most cases, it's obvious which email address I want to use. On disk, my projects are organized in a folder structure like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/code/
├── work/
│   ├── client_a
│   │   ├── projetc_a
│   │   └── projetc_b
│   ├── internal
│   │   ├── project_c
│   │   └── project_d
│   └── client_b
└── personal/
    ├── project_a
    └── project_a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my &lt;code&gt;.gitconfig&lt;/code&gt; file I include private or work-related additional configuration depending on the directory path of a repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;[&lt;span class="n"&gt;includeIf&lt;/span&gt; &lt;span class="s2"&gt;"gitdir:code/personal/"&lt;/span&gt;]
  &lt;span class="n"&gt;path&lt;/span&gt; = ~/.&lt;span class="n"&gt;gitconfig_personal&lt;/span&gt;
[&lt;span class="n"&gt;includeIf&lt;/span&gt; &lt;span class="s2"&gt;"gitdir:code/work/"&lt;/span&gt;]
  &lt;span class="n"&gt;path&lt;/span&gt; = ~/.&lt;span class="n"&gt;gitconfig_work&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These &lt;em&gt;personal&lt;/em&gt; and &lt;em&gt;work&lt;/em&gt; sub-configurations only configure &lt;code&gt;user.email&lt;/code&gt;. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# .gitconfig_personal
&lt;/span&gt;[&lt;span class="n"&gt;user&lt;/span&gt;]
  &lt;span class="n"&gt;email&lt;/span&gt; = &lt;span class="n"&gt;me&lt;/span&gt;@&lt;span class="n"&gt;work&lt;/span&gt;.&lt;span class="n"&gt;internet&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This spares manually running &lt;code&gt;git work&lt;/code&gt; or &lt;code&gt;git personal&lt;/code&gt; in every repo. Cloning a new project at work into a subdirectory of &lt;code&gt;~/code/work&lt;/code&gt;, the configuration already tells Git to use my work email.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub 🐙
&lt;/h2&gt;

&lt;p&gt;Your work email needs to be added to your GitHub account: &lt;a href="https://github.com/settings/emails"&gt;https://github.com/settings/emails&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub UI
&lt;/h3&gt;

&lt;p&gt;Merging pull requests in the GitHub UI or accepting change suggestions also creates a commit. Similar to Git itself, GitHub doesn't associate commits with your account, but associates the account via the email address the commit is made with 🤯. For UI interactions, this is a bit tricky to get right.&lt;/p&gt;

&lt;p&gt;On the first UI interaction in a repository, GitHub shows a dropdown to select the email address to use for the resulting commit. It then stores this email address for that repository and continues to use it for all UI interactions without showing a dropdown to change it ever again. This means, after having used the &lt;em&gt;wrong&lt;/em&gt; email once, it is a bit hard to get out of this again.&lt;/p&gt;

&lt;p&gt;I already reached out to GitHub support for this problem, but they could not help me. For now, the only &lt;em&gt;"hack"&lt;/em&gt; I found to switch the email again is to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the respective repository&lt;/li&gt;
&lt;li&gt;Click on the 🖊 icon to edit a file (e.g. &lt;code&gt;README.md&lt;/code&gt;) in the web editor&lt;/li&gt;
&lt;li&gt;You can choose your email again from a dropdown when commiting&lt;/li&gt;
&lt;li&gt;GitHub will continue to use this email from now on for this repo&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; this email juggling in the UI is what worked for me back then, it may already be different now.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a&gt;1&lt;/a&gt;: &lt;a href="https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address#about-commit-email-addresses"&gt;https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address#about-commit-email-addresses&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>github</category>
      <category>git</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Script up your projects</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Tue, 02 Feb 2021 18:08:51 +0000</pubDate>
      <link>https://forem.com/bitcrowd/script-up-your-projects-lhn</link>
      <guid>https://forem.com/bitcrowd/script-up-your-projects-lhn</guid>
      <description>&lt;p&gt;At &lt;a href="https://bitcrowd.net/"&gt;bitcrowd&lt;/a&gt;, we &lt;a href="https://github.com/bitcrowd/tickety-tick#readme"&gt;love conventions&lt;/a&gt;. Being an agency, we jump projects, codebases, frameworks and languages quite a lot - to a point where it's impossible to keep all the details about every project one's head. It's not necessary to actually have this knowledge in practise though. &lt;em&gt;Some&lt;/em&gt; people knowing the details of &lt;em&gt;some&lt;/em&gt; projects is enough. They can onboard others who then slowly forget the details of their previous projects… A well-established, semi-stable cycle of knowledge transfer with some silos here and some information lost there - but overall working.&lt;/p&gt;

&lt;p&gt;Still, there is this nice and fluffy feeling of coming to a new project, seamlessly setting it up and immediately finding your way around. Taking developers by their hands upon entry, these &lt;a href="https://en.wikipedia.org/wiki/Hygge"&gt;hygge&lt;/a&gt; codebases appear nice, friendly and motivating. And developer happiness aside, even on the dark and cold business side of things, people coming to these "&lt;em&gt;welcoming"&lt;/em&gt; codebases are probably likely to be more productive with the head start they get there.&lt;/p&gt;

&lt;p&gt;How can we get to this shiny place?&lt;/p&gt;

&lt;h2&gt;
  
  
  Conventions ⚖️
&lt;/h2&gt;

&lt;p&gt;Notorious self-optimization and reaching for ultimate &lt;em&gt;"productivity"&lt;/em&gt; are neoliberal derailments. But decluttering daily tasks from unnecessary cognitive overhead, can actually have a calming, decelerating effect on our personal work life: it gives us a cleaner, virtual &lt;em&gt;"desk"&lt;/em&gt;, less stress and easier time to set details aside and focus on the important things.&lt;/p&gt;

&lt;p&gt;In software development, conventions can be a useful tool to reduce the cognitive effort involved when switching between projects. Backed by a known outline of what to expect where, developers can, for the moment, put aside the nitty-gritty details of which version X of framework Y a project uses and instead focus on things like &lt;em&gt;domain&lt;/em&gt; or &lt;em&gt;datamodel&lt;/em&gt; to easier wrap their head around the new problem space. Utilizing agreed upon patterns, we can take load off peoples' shoulders for the ultimate &lt;em&gt;"hygge"&lt;/em&gt; developer experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Situation 🚣
&lt;/h2&gt;

&lt;p&gt;In agency work, with its fast and frequent project switches, the &lt;em&gt;"onboarding"&lt;/em&gt; and "&lt;em&gt;getting started"&lt;/em&gt; phase on a codebase are especially crucial. But product teams shouldn't overwhelm new members with days of fiddling with different package managers either.&lt;/p&gt;

&lt;h3&gt;
  
  
  First Contact 🐣
&lt;/h3&gt;

&lt;p&gt;Coming to a new project. What are the first steps you &lt;strong&gt;always&lt;/strong&gt; take?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You most likely have a look at the README first &lt;em&gt;(hopefully it's a good one)&lt;/em&gt; 🔦&lt;/li&gt;
&lt;li&gt;You search the README for installation and setup instructions &lt;em&gt;(the classic "getting started")&lt;/em&gt; 🔎&lt;/li&gt;
&lt;li&gt;You spend a few ~minutes~ hours copy-pasting things from the README into your shell to get to a state where you can finally run the project 🚜&lt;/li&gt;
&lt;li&gt;You nag your colleagues for the steps which are missing, outdated or simply not documented 🕵️&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All in all you pretty much spend around half a day setting up the project and already lost the joy of contributing to it. Time, effort and motivation you could have spent on getting to know the domain better and that will be missing in later phases of the project.&lt;/p&gt;

&lt;p&gt;A sad situation for a group of people aiming to utilize computer to &lt;em&gt;"solve problems"&lt;/em&gt;. Projects with a bad setup and onboarding experience are likely to distract and discourage people. Coming out of a rough setup safari, one may feel insecure and as if one knew &lt;em&gt;"nothing"&lt;/em&gt; about the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day-in day-out 🌚🌝
&lt;/h3&gt;

&lt;p&gt;Once actually working on a project, one has to run certain development tasks now and then: database migrations, the test suite, managing translations, etc. One may want utilize all possible brain capabilities on memorizing those for each possible framework or alternatively just search up and down the shell history for the one command one at some point managed to successfully copy-and-paste. But what if you don't even know what to search for?&lt;/p&gt;

&lt;p&gt;A classic example: You come back to the project after a few days on a different one. You fetch the latest changes on the &lt;code&gt;main&lt;/code&gt; branch and then… Run the migrations? Update your JavaScript version? Update packages and then run migrations? Is it &lt;code&gt;npm install&lt;/code&gt; or &lt;code&gt;yarn install&lt;/code&gt;? A steam of questions and decisions to take before you can even start your editor or run the first test. And there is also the extra cognitive overload of context switches within the project: switching between feature branches, pairing with a colleague on their ticket, etc. &lt;/p&gt;

&lt;p&gt;Agreeing on conventions, on a common way to do and approach things across projects may reduce this overload and give people an easier time thought the day 🐖.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scripts to the Rescue 🚑
&lt;/h2&gt;

&lt;p&gt;People at Github made an attempt to fix this situation: &lt;a href="https://github.com/github/scripts-to-rule-them-all"&gt;scripts to rule them all&lt;/a&gt;. The idea is to have common set of executable scripts for common developer tasks in a &lt;code&gt;script/&lt;/code&gt; directory in the root of every project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A consistent bootstrapping experience across all our projects reduces friction and encourages contribution.¹&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While every project may use different tools or languages, the &lt;code&gt;script&lt;/code&gt; directory consistently following the same pattern everywhere gives developers something to hold on to. Scripts as an anchor in the sea of cognitive overload after checking out a codebase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;script/bootstrap&lt;/code&gt; to install/update dependencies&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script/setup&lt;/code&gt; to set up a project for the first time&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script/update&lt;/code&gt; to update a project to run at its current version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script/test&lt;/code&gt; to run tests&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;script/console&lt;/code&gt; to opens a console&lt;/li&gt;
&lt;li&gt;…&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Technical setup instructions can of course live as 100+ copy-pasteable steps in the README. Putting them into a script has roughly the same effort but a huge benefit: it can do the work for future developers 🤖. Scripts can work well as a runnable documentation for a project's tooling setup. Imagine switching between Python and JavaScript projects. How to run migrations here, what are the default CLI flags there? Instead of infinitely searching the shell history, simple scripts in the project can wrap and document common developer tasks.&lt;/p&gt;

&lt;p&gt;The selection of files in your &lt;code&gt;script&lt;/code&gt; directory of course depends on every team's individual situation and workflow. Just go with &lt;a href="https://en.wikipedia.org/wiki/Marie_Kondo"&gt;Marie Kondo&lt;/a&gt;, get rid of scripts which don't &lt;em&gt;"spark joy"&lt;/em&gt; and add others where you feel pain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommendations 🛒
&lt;/h3&gt;

&lt;p&gt;We adapted this pattern for our own workflows at bitcrowd. Here are some loose recommendations on what we find useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;script/test-e2e&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a lot of projects, high level end-to-end tests are slower. So it's nice to be able to run them with a separate command. With that, &lt;code&gt;script/test&lt;/code&gt; can focus on running unit tests only, finish faster and provide important feedback earlier.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;script/lint&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We are obsessed with linters. Running them as part of the test suite does not fit our way of working. For us it makes sense to run tests and linters separately as they hint at different problems in your code. Maybe one wants to not care about linting until one has a working implementation or tests and prefers to do linting and code cosmetics later?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;script/format&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some ecosystems support formatters. If machines can do the formatting for us, why not accept their help?&lt;/p&gt;

&lt;h3&gt;
  
  
  Your own 🎨
&lt;/h3&gt;

&lt;p&gt;Add your own scripts, like &lt;code&gt;script/deploy&lt;/code&gt;, &lt;code&gt;script/psql&lt;/code&gt;, etc., there are no limits. Come up with whatever suits your organization and team. The only thing: &lt;strong&gt;stay consistent&lt;/strong&gt; and have conventions. That's where the real benefits lie.&lt;/p&gt;

&lt;p&gt;For instance &lt;code&gt;script/manage&lt;/code&gt; to facilitate the &lt;a href="https://docs.djangoproject.com/en/3.1/ref/django-admin/"&gt;&lt;code&gt;manage.py&lt;/code&gt;&lt;/a&gt; utility in &lt;a href="https://www.djangoproject.com/"&gt;Django&lt;/a&gt; projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="c"&gt;# Run manage.py tasks&lt;/span&gt;
&lt;span class="c"&gt;# Usage: scripts/manage [...args]&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; errexit
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nounset

&lt;span class="nv"&gt;SCRIPTDIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&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="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SCRIPTDIR&lt;/span&gt;&lt;span class="s2"&gt;/.."&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;poetry run python manage.py &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on a project which requires a specific version of a &lt;a href="https://postgis.net/"&gt;PostGIS&lt;/a&gt; database, using a utility script to to conveniently run the database in a &lt;a href="https://www.docker.com/"&gt;Docker&lt;/a&gt; container. Developers may run &lt;code&gt;script/db start&lt;/code&gt; to start the database and &lt;code&gt;script/db stop&lt;/code&gt; to stop it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; errexit
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; nounset
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; pipefail

&lt;span class="nv"&gt;CONTAINER_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;project_x_db

find_container&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  docker ps &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

start_db&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_container&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exec &lt;/span&gt;docker run &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--tty&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;C.UTF-8 &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_USER&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$POSTGRES_DB&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--publish&lt;/span&gt; 5432:5432 &lt;span class="se"&gt;\&lt;/span&gt;
      postgis/postgis:9.6-2.5-alpine &lt;span class="se"&gt;\&lt;/span&gt;
      postgres
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;exec &lt;/span&gt;docker start &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

stop_db&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;exec &lt;/span&gt;docker stop &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CONTAINER_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

print_usage&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Usage: db [&amp;lt;option&amp;gt;]"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Options:"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  start   Start database."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  stop    Stop database."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  --help  Show this message and exit."&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$# &lt;/span&gt;&lt;span class="nt"&gt;-lt&lt;/span&gt; 1 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;print_usage
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

case&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;start&lt;span class="p"&gt;)&lt;/span&gt;
    start_db
    &lt;span class="p"&gt;;;&lt;/span&gt;
  stop&lt;span class="p"&gt;)&lt;/span&gt;
    stop_db
    &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    print_usage
    &lt;span class="nb"&gt;exit&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How to do scripts? 🚌
&lt;/h2&gt;

&lt;p&gt;Who would ever blindly run a script on their machine? Better take a look at the contents first. If written, formatted and documented with care, scripts work as a great way to consistently document development workflows. And since we never get things right the first time: continue to update your scripts (e.g. when new people are onboarded and run into issues). Integrating your scripts into your daily workflows and running them regularly also helps to iron out rough edges. We for instance usually run &lt;code&gt;script/test&lt;/code&gt;, &lt;code&gt;script/test-e2e&lt;/code&gt; and &lt;code&gt;script/lint&lt;/code&gt; as part of our CI pipeline.&lt;/p&gt;

&lt;p&gt;When it comes to writing scripts, pick whatever suits your usecase and what you feel comfortable with. For the sake of compatibility, &lt;a href="https://www.gnu.org/software/bash/"&gt;Bash&lt;/a&gt; can be a good choice. But be aware, it can be tricky at times… ⚠️:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The weird thing about shell scripts is that even strong advocates of good practices gladly forget all they know when it comes to shell scripting.²&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Linting your shell scripts with &lt;a href="https://github.com/koalaman/shellcheck"&gt;Shellcheck&lt;/a&gt; can improve the situation a lot though. It is also a great resource and opportunity to learn about best practices.&lt;/p&gt;

&lt;p&gt;Here are some resources we found useful for leveling up our scripting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/koalaman/shellcheck"&gt;https://github.com/koalaman/shellcheck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/thiht/shell-scripts-matter"&gt;https://dev.to/thiht/shell-scripts-matter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kvz.io/bash-best-practices.html"&gt;https://kvz.io/bash-best-practices.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thoughtbot.com/blog/shell-script-suggestions-for-speedy-setups"&gt;https://thoughtbot.com/blog/shell-script-suggestions-for-speedy-setups&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Scripting is not a silver bullet, but it can make developer lives a bit easier. And it's a great rabbit hole to get lost in 🕳&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a&gt;1&lt;/a&gt;: &lt;a href="https://github.blog/2015-06-30-scripts-to-rule-them-all/"&gt;https://github.blog/2015-06-30-scripts-to-rule-them-all/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a&gt;2&lt;/a&gt;: &lt;a href="https://blog.thibaut-rousseau.com/2017/12/03/shell-scripts-matter.html"&gt;https://blog.thibaut-rousseau.com/2017/12/03/shell-scripts-matter.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>bash</category>
      <category>scripting</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Prepending Modules to the Rescue</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Thu, 26 Nov 2020 14:26:25 +0000</pubDate>
      <link>https://forem.com/bitcrowd/prepending-modules-to-the-rescue-24h6</link>
      <guid>https://forem.com/bitcrowd/prepending-modules-to-the-rescue-24h6</guid>
      <description>&lt;p&gt;Ruby's ability to &lt;em&gt;"overlay"&lt;/em&gt; default implementations of constants, methods and variables via the &lt;a href="https://ruby-doc.org/core-2.7.1/Module.html#method-i-prepend"&gt;&lt;code&gt;prepend&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://ruby-doc.org/core-2.7.1/Module.html#method-i-prepend_features"&gt;&lt;code&gt;prepend_features&lt;/code&gt;&lt;/a&gt; method on &lt;code&gt;Module&lt;/code&gt; can be a helpful tool when dealing with gems overriding setter methods for attributes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario 🧫
&lt;/h2&gt;

&lt;p&gt;On a &lt;a href="https://rubyonrails.org/"&gt;Ruby on Rails&lt;/a&gt; project, we were recently migrating from the &lt;a href="https://github.com/attr-encrypted/attr_encrypted"&gt;&lt;code&gt;attr_encrypted&lt;/code&gt;&lt;/a&gt; to the &lt;a href="https://github.com/ankane/lockbox"&gt;&lt;code&gt;lockbox&lt;/code&gt;&lt;/a&gt; gem for encrypting database fields. For live applications which cannot accept hours of downtime, the migration path is a multi-step process which includes a period where both gems are used side by side until all data has been migrated from the old columns to the new columns.&lt;/p&gt;

&lt;p&gt;Both gems integrate into Rails models via their own macro-style methods one is supposed to add to the models' class definitions:&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;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;attr_encrypted&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
  &lt;span class="n"&gt;attr_encrypted&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;

  &lt;span class="n"&gt;encrypts&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;migrating: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, both gems then dynamically generate the respective getter and setter methods for the attributes (&lt;code&gt;email&lt;/code&gt;, &lt;code&gt;email=&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt; and &lt;code&gt;phone=&lt;/code&gt; in this example).&lt;/p&gt;

&lt;p&gt;In addition to that, we were also overriding the setter methods &lt;code&gt;email=&lt;/code&gt; and &lt;code&gt;phone=&lt;/code&gt; ourselves to do some normalization on the provided values before assigning them. Combining this with the generated setters from &lt;code&gt;attr_encrypted&lt;/code&gt; introduces a lot of fuzz: in what order are the implementations called - if at all - and what does &lt;code&gt;super&lt;/code&gt; mean in which context? In order to eliminate all this confusion from the start, we previously decided to make our own implementation the &lt;em&gt;"source of truth"&lt;/em&gt; and instead of relying on &lt;code&gt;super&lt;/code&gt; calls, just integrate the respective parts of &lt;code&gt;attr_encrypted&lt;/code&gt;'s implementation into our own:&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;phone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;normalized_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Normalizers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted_phone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalized_number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;instance_variable_set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:@phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;normalized_number&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;h2&gt;
  
  
  Problem 💥
&lt;/h2&gt;

&lt;p&gt;Now, during the migration phase were we utilize both gems in parallel, our own implementation would of course also need to integrate the internals of both gems in our own overriden implementation. Plus we would need to ensure the &lt;code&gt;attr_encrypted&lt;/code&gt; related code is removed again once the migration phase is over 🤯.&lt;/p&gt;

&lt;p&gt;This seemed overwhelming and just way too many things to take care of for our own tiny model implementation. Integrating gems should ideally not interfere too much with our own plans to normalize attributes before assigning them. In addition to that, integrating so deeply with a gem, that understanding the code required reading the gems internals beyond the &lt;em&gt;"normal"&lt;/em&gt; instructions in the README also comes with a high price on maintainability.&lt;/p&gt;

&lt;p&gt;So we needed to find another way to use both gems in parallel while also guaranteeing our values are normalized &lt;strong&gt;before&lt;/strong&gt; assigning and &lt;strong&gt;before&lt;/strong&gt; encrypting them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rescue 🚑
&lt;/h2&gt;

&lt;p&gt;A &lt;a href="https://blog.arkency.com/2016/02/using-anonymous-modules-and-prepend-to-work-with-generated-code/"&gt;post on the arkency blog&lt;/a&gt; describes how Ruby's &lt;a href="https://ruby-doc.org/core-2.7.1/Module.html#method-i-prepend"&gt;&lt;code&gt;prepend&lt;/code&gt;&lt;/a&gt; method on &lt;code&gt;Module&lt;/code&gt; can be utilized to override or better &lt;em&gt;"overlay"&lt;/em&gt; methods added directly onto the class by a gem. One can prepend an anonymous module inline with the own implementation to either fully override the gem's implementation or just &lt;em&gt;"prepend"&lt;/em&gt; one's own implementation and then call &lt;code&gt;super()&lt;/code&gt; to still invoke the code generated by the gem.&lt;/p&gt;

&lt;p&gt;We simply want to &lt;em&gt;"prepend"&lt;/em&gt; our own normalization step before the gems start to do their magic and ideally don't want to get into the details of what they are actually doing. So utilizing &lt;code&gt;super()&lt;/code&gt; after the normalization step fits our use case perfectly. In the model class definition, this could look like this:&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;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;attr_encrypted&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
  &lt;span class="n"&gt;attr_encrypted&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;

  &lt;span class="n"&gt;encrypts&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;migrating: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Module&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;normalized_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Normalizers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PhoneNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized_number&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="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;em&gt;"overlays"&lt;/em&gt; or &lt;em&gt;"prepends"&lt;/em&gt; our normalization step before the class' implementation of the setter method, even when it's changed by any of the included gems. So we make sure the value is normalized before it is assigned and the gems' implementation of the setter invoked afterwards without any need for us to fiddle with internal details.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;strong&gt;Note&lt;/strong&gt; however, that for the sake of readability and consistency, our model class definition follows Rubocop's &lt;a href="https://github.com/rubocop-hq/rails-style-guide#macro-style-methods"&gt;Rails Style Guide&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Group macro-style methods (&lt;code&gt;has_many&lt;/code&gt;, &lt;code&gt;validates&lt;/code&gt;, etc) in the beginning of the class definition.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Following this class layout, our own implementation is sure to be processed after the gems' methods were defined and we are prepending our normalization step before the final definition of the setter method. Ordering the definitions differently in our class would impact the value of &lt;code&gt;super()&lt;/code&gt; here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup 🧽
&lt;/h2&gt;

&lt;p&gt;While prepending an anonymous module directly inside the class definitions works perfectly fine for our use case here, it still looks very verbose and suspiciously distracting for anyone reading over the model definition. And as we made use of this technique in multiple models within the project, we extracted the boilerplate into a &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/Concern.html"&gt;model concern&lt;/a&gt;. The goal was to hide the details of the prepending trick while at the same time making the normalization step more visible and explicit to the reader.&lt;/p&gt;

&lt;p&gt;We extracted a more general &lt;code&gt;Normalizable&lt;/code&gt; concern. We already had multiple normalizer classes in the project. They all follow the same pattern and expose a single &lt;code&gt;normalize&lt;/code&gt; class method as their public API. So it just made sense tie the implementation of the &lt;code&gt;Normalizable&lt;/code&gt; model concern close to those normalizers:&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;module&lt;/span&gt; &lt;span class="nn"&gt;Normalizable&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;class_methods&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kp"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
      &lt;span class="n"&gt;prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Module&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="n"&gt;define_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="kp"&gt;attr&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="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
          &lt;span class="n"&gt;normalized_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;with&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:normalize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized_value&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="k"&gt;end&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;Utilizing this, we can change the previous example to:&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;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Normalizable&lt;/span&gt;

  &lt;span class="n"&gt;attr_encrypted&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;
  &lt;span class="n"&gt;attr_encrypted&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;

  &lt;span class="n"&gt;encrypts&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;migrating: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;normalize&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="no"&gt;Normalizers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PhoneNumber&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hides the internal details of our own implementation of the setter while still making it explicit that we are doing a normalization on the attribute. Similar to the original implementation inlining &lt;code&gt;prepend&lt;/code&gt; with an anonymous module, this approach of course still only works as intended if the &lt;code&gt;normalize&lt;/code&gt; macro in the class definition is defined &lt;strong&gt;after&lt;/strong&gt; any other setter methods generated by gems are defined. However, in our case it seemed most fitting to place the `normalize calls at the end of the macros section anyways.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Improving callable service objects with private constructors in Ruby</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Wed, 25 Nov 2020 10:06:41 +0000</pubDate>
      <link>https://forem.com/bitcrowd/improving-callable-service-objects-with-private-constructors-in-ruby-p89</link>
      <guid>https://forem.com/bitcrowd/improving-callable-service-objects-with-private-constructors-in-ruby-p89</guid>
      <description>&lt;p&gt;We recently found a nice practical use case for private class methods in Ruby: the constructor methods of what we will refer to here as &lt;em&gt;"callable services"&lt;/em&gt; in &lt;a href="https://rubyonrails.org/"&gt;Ruby on Rails&lt;/a&gt; projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Callable Services ☎️
&lt;/h2&gt;

&lt;p&gt;First off: what do we mean with "&lt;em&gt;callable services"&lt;/em&gt; in Ruby on Rails applications?&lt;/p&gt;

&lt;h3&gt;
  
  
  Drawers 🗄
&lt;/h3&gt;

&lt;p&gt;In the real world, hardly any Ruby on Rails web application only consists of simple, atomic &lt;a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete"&gt;CRUD&lt;/a&gt; (create, read, update, delete) operations on plain resources. Even the famous &lt;a href="https://www.youtube.com/watch?v=Gzj723LkRJY&amp;amp;feature=youtu.be"&gt;blog engine built in 15 minutes&lt;/a&gt; may at some point incorporate logic beyond the complexity of creating or deleting posts. Take for instance translations, previews, comment moderation, etc. Real world applications model a specific domain and therefore include control flows, procedures and &lt;em&gt;"business logic"&lt;/em&gt; tied to that domain. These bits are in fact what makes an app unique and interesting.&lt;/p&gt;

&lt;p&gt;When it comes to organizing code, Rails people traditionally aim for &lt;a href="http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model"&gt;&lt;em&gt;"skinny controllers and fat models"&lt;/em&gt;&lt;/a&gt;. But as models packed with responsibilities beyond modeling the domain soon tend to get overweight and hard to maintain, the community started to reach for &lt;a href="https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models"&gt;additional concepts and patterns to organize their domain specific code&lt;/a&gt;. One &lt;em&gt;"drawer"&lt;/em&gt; one may come across in a lot of Rails applications are &lt;em&gt;"services"&lt;/em&gt;, also referred to as &lt;a href="https://avdi.codes/service-objects/"&gt;&lt;em&gt;"service objects"&lt;/em&gt;&lt;/a&gt; or &lt;em&gt;"procedures"&lt;/em&gt;. They are usually organized under the &lt;code&gt;app/services/&lt;/code&gt; directory and encapsulate functionality to handle domain-specific logic, such as checking our a cart, registering for the site or starting a subscription. ¹&lt;/p&gt;

&lt;p&gt;Services are usually implemented as POROs ("plain old Ruby objects") which, upon a given input, perform a set of operations and return a predictable response. They are easy to unit test and help developers to maintain a sense for the bigger picture by hiding away the internal details in what appears to the outside as a large black-box-function. That is why people started to implement them as classes exposing only a single method, &lt;code&gt;call&lt;/code&gt; or &lt;code&gt;run&lt;/code&gt;. Depending on its internals and their complexity a service may create a new instance for each call or just utilize a single class method for its work:&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;Authenticator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# complex logic&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;# invoking the service:&lt;/span&gt;
&lt;span class="no"&gt;Authenticator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same service implemented to use a new instance for each call:&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;Authenticator&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="c1"&gt;# complex logic, operate on @user&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;# invoking the service:&lt;/span&gt;
&lt;span class="no"&gt;Authenticator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conventions 🧘
&lt;/h3&gt;

&lt;p&gt;At bitcrowd we - as you know - &lt;a href="https://github.com/bitcrowd/tickety-tick"&gt;love conventions&lt;/a&gt; and therefore usually strive for a common API for our classes in &lt;code&gt;app/services&lt;/code&gt; in Rails projects. So even if we don't know what's in the box, we at least know we're dealing with a box… Picking up the previous example, this could look like this:&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;Authenticator&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="c1"&gt;# complex logic&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;private_helper_method&lt;/span&gt;
    &lt;span class="c1"&gt;# some bits of logic, can operate on user&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;So within the project all services follow the same general structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a constructor takes all the core input data the service needs to do its work&lt;/li&gt;
&lt;li&gt;a single exposed &lt;code&gt;call&lt;/code&gt; method invokes the service to perform the actual work&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simplifications 🧹
&lt;/h3&gt;

&lt;p&gt;The functionality encapsulated within a service is seen as one operational unit on the outside. So we probably won't interfere between initializing the service and calling it. If the data needed to be manipulated between &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;call&lt;/code&gt;, we should probably rather think about drawing the boundaries between our objects differently instead. But having things clearly encapsulated, we could also simplify the service' API to &lt;code&gt;Service.call(&amp;lt;input-data&amp;gt;)&lt;/code&gt; and hide the implementation details of it using a new instance for each call inside of it. Since Ruby 2.7, we also make use of the 3 dots argument forwarding syntax ² - also referred to as &lt;a href="https://bugs.ruby-lang.org/issues/16253"&gt;&lt;em&gt;"forward everything"&lt;/em&gt;&lt;/a&gt; - for the &lt;code&gt;.call&lt;/code&gt; class method:&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;Authenticator&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="c1"&gt;# complex logic&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;This API makes our service more streamlined and predictable. The service is easier to read and less verbose on the outside, hiding implementation details inside the class itself. We're also less likely to accidentally sneak code between &lt;code&gt;new&lt;/code&gt; and &lt;code&gt;call&lt;/code&gt; when invoking the service. And while it's aesthetically pleasing on the eye, it also allows us to write less verbose expectations in our unit tests for code which interacts with the service:&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="c1"&gt;# Before&lt;/span&gt;
&lt;span class="n"&gt;let&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:service_instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;instance_double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Authenticator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'calls the service'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Authenticator&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;and_return&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service_instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service_instance&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# After&lt;/span&gt;
&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="s1"&gt;'calls the service'&lt;/span&gt;
  &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Authenticator&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt; &lt;span class="n"&gt;receive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:call&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&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;While we previously needed two expectations to ensure both, the service being initialized with the right data and then being invoked, we can now do both in one step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Communication 📢
&lt;/h2&gt;

&lt;p&gt;Even with the new simpler API, our services can of course still be called in "the old" way, initializing and calling the service in two steps. The API for this approach is public and aside from examples in the code or documentation, we don't have anything at hand to ensure the service is used as intended. People may still happily do &lt;code&gt;Authenticator.new(user).call&lt;/code&gt; and use instance doubles in their tests… The new API only gives a hint on how to use services in the project, it does not actually encourage or enforce one unified style.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private Constructors to the Rescue 🚒
&lt;/h3&gt;

&lt;p&gt;Turns out we can make use of Ruby's &lt;a href="https://ruby-doc.org/core-2.7.1/Module.html#method-i-private_class_method"&gt;&lt;code&gt;private_class_method&lt;/code&gt;&lt;/a&gt; method on &lt;code&gt;Module&lt;/code&gt; to hide the constructor and make our intentions on how to use the services more obvious:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Makes existing class methods private. Often used to hide the default constructor new.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Adapting our example:&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;Authenticator&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;private_class_method&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="c1"&gt;# actual logic&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;With this we introduce a new problem though: the visual overhead of the additional boilerplate code makes the actual service implementation harder to read and understand. One way to circumvent this and allow the readers to stay focussed on the actual business logic, would be extracting the boilerplate into a &lt;a href="https://api.rubyonrails.org/classes/ActiveSupport/Concern.html"&gt;concern&lt;/a&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;module&lt;/span&gt; &lt;span class="nn"&gt;Callable&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;private_class_method&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;class_methods&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="k"&gt;end&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;We can then shorten the service to:&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;Authenticator&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Callable&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="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="c1"&gt;# actual logic&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;This ensures our service is now used as intended. Trying to call its parts separately as &lt;code&gt;Authenticator.new(user).call&lt;/code&gt; results in an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NoMethodError: private method `new' called for Authenticator:Class
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing so, we can a clean and concise outer API for our services while not sacrificing readability on its internals. Invoking a service with its single exposed &lt;code&gt;call&lt;/code&gt; class method ensure we pass the initial data to set up the state it needs to perform its work and then immediately trigger the actual &lt;em&gt;"work"&lt;/em&gt; part.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;private_class_method&lt;/code&gt; to hide the initializer of your service objects for a clearer API and less boilerplate in tests 💅.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1: &lt;a href="https://blog.appsignal.com/2020/06/17/using-service-objects-in-ruby-on-rails.html"&gt;https://blog.appsignal.com/2020/06/17/using-service-objects-in-ruby-on-rails.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;2: &lt;a href="https://github.com/ruby/ruby/blob/v2_7_0/NEWS#label-Other+miscellaneous+changes"&gt;https://github.com/ruby/ruby/blob/v2_7_0/NEWS#label-Other+miscellaneous+changes&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Folding sections of Markdown in Vim</title>
      <dc:creator>Max</dc:creator>
      <pubDate>Mon, 25 May 2020 07:22:45 +0000</pubDate>
      <link>https://forem.com/bitcrowd/folding-sections-of-markdown-in-vim-23ne</link>
      <guid>https://forem.com/bitcrowd/folding-sections-of-markdown-in-vim-23ne</guid>
      <description>&lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; Vim is able to fold sections or Markdown on their headings 💡&lt;/p&gt;

&lt;p&gt;Modern version of &lt;a href="https://www.vim.org/"&gt;Vim&lt;/a&gt; and &lt;a href="https://neovim.io/"&gt;Neovim&lt;/a&gt; support folding &lt;code&gt;.md&lt;/code&gt; &lt;a href="https://en.wikipedia.org/wiki/Markdown"&gt;Markdown&lt;/a&gt; documents on their section headings &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;##&lt;/code&gt;, &lt;code&gt;###&lt;/code&gt; etc. out of the box, even without installing the additional &lt;a href="https://github.com/masukomi/vim-markdown-folding"&gt;vim-markdown-folding&lt;/a&gt; plugin.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is folding 🤔
&lt;/h2&gt;

&lt;p&gt;Vimʼs documentation describes &lt;em&gt;“folding”&lt;/em&gt; as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Folding is used to show a range of lines in the buffer as a single line on the
screen.  Like a piece of paper which is folded to make it shorter:

    +------------------------+
    | line 1         |
    | line 2         |
    | line 3         |
    |_______________________ |
    \            \
     \________________________\
     / folded lines       /
    /________________________/
    | line 12        |
    | line 13        |
    | line 14        |
    +------------------------+

The text is still in the buffer, unchanged.  Only the way lines are displayed
is affected by folding.

The advantage of folding is that you can get a better overview of the
structure of text, by folding lines of a section and replacing it with a line
that indicates that there is a section.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When editing large documents, I personally find it handy to temporarily fold and &lt;em&gt;“hide away”&lt;/em&gt; certain parts I currently donʼt care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Folding Markdown 🗂
&lt;/h2&gt;

&lt;p&gt;Out of the box, Vim and Neovim currently wonʼt know &lt;em&gt;“how”&lt;/em&gt; to fold Markdown sections. Their included default &lt;a href="https://github.com/tpope/vim-markdown"&gt;vim-markdown&lt;/a&gt; filetype and syntax plugin however offers an undocumented setting to enable just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let g:markdown_folding = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is an &lt;a href="https://github.com/tpope/vim-markdown/pull/135"&gt;open pull request&lt;/a&gt; include this in the official documentation, but it is not merged yet.&lt;/p&gt;

&lt;p&gt;Flicking this switch turns my &lt;code&gt;.md&lt;/code&gt; buffers into something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+--- 32 lines: ## What is folding 🤔···········································
+--- 23 lines: ## Folding Markdown 🗂···········································
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pressing &lt;code&gt;zR&lt;/code&gt; will recursively unfold all sections again. From there on you may continue to happily fold and unfold. &lt;a href="https://twitter.com/nelstrom"&gt;Drew Neil&lt;/a&gt; from &lt;a href="http://vimcasts.org"&gt;Vimcasts&lt;/a&gt; put together a nice and short &lt;a href="http://vimcasts.org/episodes/how-to-fold/"&gt;overview&lt;/a&gt; over the most important folding commands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;command&lt;/th&gt;
&lt;th&gt;effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;open current fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zO&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;recursively open current fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;close current fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;recursively close current fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;za&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;toggle current fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;recursively open/close current fold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;reduce &lt;code&gt;foldlevel&lt;/code&gt; by one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;close all folds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;increase &lt;code&gt;foldlevel&lt;/code&gt; by one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;zR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;open all folds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  First look 👀
&lt;/h3&gt;

&lt;p&gt;With Markdown-folding enabled, Vim will default to the fully &lt;em&gt;“folded”&lt;/em&gt; view when opening a Markdown buffer. I personally prefer to start with the &lt;em&gt;“full”&lt;/em&gt; document though. According to &lt;a href="https://stackoverflow.com/questions/8316139/how-to-set-the-default-to-unfolded-when-you-open-a-file"&gt;this Stackoverflow thread&lt;/a&gt; there are a few ways out of this behavior.&lt;/p&gt;

&lt;p&gt;I went for setting a custom &lt;code&gt;foldlevelstart&lt;/code&gt; value in my configuration. It instructs Vim how or if a buffer should be folded when one starts editing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;'foldlevelstart' 'fdls' number (default: -1)
            global
    Sets 'foldlevel' when starting to edit another buffer in a window.
    Useful to always start editing with all folds closed (value zero),
    some folds closed (one) or no folds closed (99).
    This is done before reading any modeline, thus a setting in a modeline
    overrules this option.  Starting to edit a file for |diff-mode| also
    ignores this option and closes all folds.
    It is also done before BufReadPre autocommands, to allow an autocmd to
    overrule the 'foldlevel' value for specific files.
    When the value is negative, it is not used.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I went for starting with all folds open in Markdown buffers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;au FileType markdown setlocal foldlevel=99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Happy folding 🗺&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>vim</category>
      <category>writing</category>
      <category>productivity</category>
      <category>markdown</category>
    </item>
  </channel>
</rss>
