<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Piotr Ładoński</title>
    <description>The latest articles on DEV Community by Piotr Ładoński (@voodu).</description>
    <link>https://dev.to/voodu</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F390537%2Fdc1fb59b-2331-4d18-8e14-9a008cbfd013.jpg</url>
      <title>DEV Community: Piotr Ładoński</title>
      <link>https://dev.to/voodu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/voodu"/>
    <language>en</language>
    <item>
      <title>Spotify Connect, Raspberry Pi, AirPlay &amp; HomePod - because simple audio setups are boring</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Sun, 14 Dec 2025 21:39:00 +0000</pubDate>
      <link>https://dev.to/voodu/spotify-connect-raspberry-pi-airplay-homepod-because-simple-audio-setups-are-boring-19lf</link>
      <guid>https://dev.to/voodu/spotify-connect-raspberry-pi-airplay-homepod-because-simple-audio-setups-are-boring-19lf</guid>
      <description>&lt;p&gt;I could write a whole long introduction of why I needed to stream Spotify from my Raspberry Pi to HomePod Mini speaker, how I arrived at the final idea, how I was hitting constant problems with other solutions... but if you're reading this, I guess you are way more interested in the actual setup 🙂 So let's just put it very briefly:&lt;/p&gt;

&lt;p&gt;My problem: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.ofzenandcomputing.com/airplay-vs-bluetooth-cy-complete-comparison-guide/#:~:text=Quick%20Answer%3A%20Bluetooth%20typically%20uses%2040%2D60%25%20less%20battery%20than%20AirPlay%20due%20to%20lower%20power%20requirements%20and%20simpler%20processing." rel="noopener noreferrer"&gt;AirPlay drains phone battery fast&lt;/a&gt;, but it's the only option for playing Spotify on HomePod Mini. How can I AirPlay music to HomePod, but not from the phone... and still control it from the phone?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Raspberry Pi &amp;amp; Spotify Connect to the rescue! Make the Raspberry Pi the playing device and control it from the phone via Spotify Connect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tricky part... was the actual execution. It took me a significant amount of time, caused quite some frustration with solutions that didn't work, and, finally, I had to take bits from several of them to make things work. In the end, if you know what to do, it's not hard, but... you must know what to do 🙂 I'll do my best to explain the process as clearly as possible, so here we go!&lt;/p&gt;

&lt;h2&gt;
  
  
  General idea for Raspberry setup
&lt;/h2&gt;

&lt;p&gt;Basics: I'm using Raspberry Pi 5 and the newest Raspbian based on Debian 13 (Trixie).&lt;/p&gt;

&lt;p&gt;There are two main things that RPi must do&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;become a Spotify Connect device and&lt;/li&gt;
&lt;li&gt;stream audio via AirPlay to the HomePod&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Luckily, it's nothing innovative, and there are already plenty of existing solutions for both things. However, there are actually so many that you can spend a lot of time trying to combine them and get nowhere. My silver bullets turned out to be&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dtcooper.github.io/raspotify/" rel="noopener noreferrer"&gt;Raspotify&lt;/a&gt; - turning RPi into a Spotify Connect device&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://owntone.github.io/owntone-server/" rel="noopener noreferrer"&gt;Owntone&lt;/a&gt; - AirPlay-ing to the speaker&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And, one secret ingredient that glues them together - Linux named pipes.&lt;/p&gt;

&lt;p&gt;Oh, and a very important detail - Spotify Premium. Raspotify doesn't work with free accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing things
&lt;/h2&gt;

&lt;p&gt;That's probably optional, but I'd start with a good old package upgrade:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;em&gt;probably&lt;/em&gt;? Because I've run it and haven't checked if things would work without the system-wide package upgrade 🙂&lt;/p&gt;

&lt;h3&gt;
  
  
  Raspotify
&lt;/h3&gt;

&lt;p&gt;That's super easy. As &lt;a href="https://dtcooper.github.io/raspotify/" rel="noopener noreferrer"&gt;the docs&lt;/a&gt; mention, just run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nb"&gt;install &lt;/span&gt;curl &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://dtcooper.github.io/raspotify/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and that's it. Raspotify will be installed &amp;amp; started. You can verify it with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status raspotify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should also already appear as a Spotify Connect device.&lt;br&gt;
Remember, your phone, Raspberry, and HomePod need to be on the same network!&lt;/p&gt;
&lt;h3&gt;
  
  
  Owntone
&lt;/h3&gt;

&lt;p&gt;You can follow the steps from the &lt;a href="https://forums.raspberrypi.com/viewtopic.php?t=49928" rel="noopener noreferrer"&gt;forum post&lt;/a&gt;, or just trust me that I copied everything correctly and run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; - https://raw.githubusercontent.com/owntone/owntone-apt/refs/heads/master/repo/rpi/owntone.gpg | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; /usr/share/keyrings/owntone-archive-keyring.gpg &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /etc/apt/sources.list.d/owntone.list https://raw.githubusercontent.com/owntone/owntone-apt/refs/heads/master/repo/rpi/owntone-trixie.list &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;owntone &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, go to &lt;a href="http://owntone.local:3689" rel="noopener noreferrer"&gt;http://owntone.local:3689&lt;/a&gt; (or &lt;a href="http://localhost:3689" rel="noopener noreferrer"&gt;http://localhost:3689&lt;/a&gt;) and check if the web UI loads correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pipes
&lt;/h3&gt;

&lt;p&gt;Long story short, we'll create pipe files to enable communication between Raspotify and Owntune. Raspotify will be writing to them, OwnTune reading and playing that via AirPlay.&lt;br&gt;
Just run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; /srv/music &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo mkfifo&lt;/span&gt; /srv/music/raspotify-pipe &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo mkfifo&lt;/span&gt; /srv/music/raspotify-pipe.metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to create two pipes. You can use different names without any issue, but if you want to put them in a different directory, you'll need to update &lt;code&gt;library.directories&lt;/code&gt; option in Owntone configuration.&lt;br&gt;
Also, &lt;code&gt;.metadata&lt;/code&gt; pipe won't be actually used, as Raspotify is not currently able to process metadata. We created it simply to have one less error from Owntone - it just needs the file to exist.&lt;/p&gt;
&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Raspotify
&lt;/h3&gt;

&lt;p&gt;All the Raspotify options are &lt;a href="https://github.com/dtcooper/raspotify/wiki/Configuration" rel="noopener noreferrer"&gt;listed on the wiki&lt;/a&gt;. We'll need to change only 2 of them.&lt;br&gt;
Let's open the configuration file with nano to edit 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="nb"&gt;sudo &lt;/span&gt;nano /etc/raspotify/conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and set &lt;code&gt;LIBRESPOT_BACKEND&lt;/code&gt; and &lt;code&gt;LIBRESPOT_DEVICE&lt;/code&gt; as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LIBRESPOT_BACKEND=pipe
LIBRESPOT_DEVICE=/srv/music/raspotify-pipe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to uncomment them!&lt;br&gt;
The &lt;code&gt;LIBRESPOT_DEVICE&lt;/code&gt; path must match the one used for the pipes. If you decide to put pipes somewhere else (for example, &lt;code&gt;/var/run/raspotify-pipe&lt;/code&gt;), make sure that &lt;code&gt;LIBRESPOT_DEVICE&lt;/code&gt; path is the same.&lt;/p&gt;

&lt;p&gt;If you want, you can also change the name under which Raspberry will appear on Spotify Connect devices list. To do this, modify &lt;code&gt;LIBRESPOT_NAME&lt;/code&gt;, for example&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Save and close the file (&lt;code&gt;Ctrl + X&lt;/code&gt;, &lt;code&gt;Y&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Owntone
&lt;/h3&gt;

&lt;p&gt;In the bottom right of the web UI, you should see an arrow to expand the simplified output control panel. One of the devices there should be the AirPlay speaker (HomePod) that you want to use - click its icon to enable it.&lt;/p&gt;

&lt;p&gt;You can also adjust the volume here. It's quite important - at the end, I had everything working correctly, but the slider here was too low, and I thought something was still broken 🙃 The "final volume" on the HomePod is a product of the volume you set in Owntone and the value picked for the Spotify Connect device on the phone. Keep that in mind when things are too quiet/loud.&lt;/p&gt;

&lt;p&gt;To make sure all the config changes in both Raspotify and Owntune are applied, restart the services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart raspotify &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart owntone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running everything
&lt;/h2&gt;

&lt;p&gt;We're basically there. Now get your phone, open Spotify, and you should be able to select Raspberry as the Spotify Connect device 🎶&lt;br&gt;
&lt;strong&gt;Remember that Raspberry and your phone must be on the same Wi-Fi network!&lt;/strong&gt;&lt;br&gt;
Here are also some details about Spotify Connect on the Spotify support page if you're not familiar with the feature itself: &lt;a href="https://support.spotify.com/article/spotify-connect/" rel="noopener noreferrer"&gt;https://support.spotify.com/article/spotify-connect/&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  General troubleshooting
&lt;/h3&gt;

&lt;p&gt;If things are not working, &lt;code&gt;journalctl&lt;/code&gt; is your best friend in debugging. Simply open a terminal and run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; owntone &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(or &lt;code&gt;raspotify&lt;/code&gt; instead of &lt;code&gt;owntone&lt;/code&gt;)&lt;br&gt;
and you'll see some system logs from the services. Hopefully, you'll see some "error" lines that will help you fix the problem.&lt;/p&gt;

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

&lt;p&gt;As I've written at the beginning, if you know the steps, the setup should be straightforward. It's just about running two services and connecting them. Knowing the Raspberry/Linux ecosystem, the above steps will become outdated sooner than later, but I hope they will be useful for at least a moment 😄&lt;/p&gt;

&lt;p&gt;If you hit any problems, write a comment. While my Linux-fu is not high, I'll do my best to help.&lt;/p&gt;

&lt;p&gt;Happy streaming! 🎵 &lt;/p&gt;




</description>
      <category>raspberrypi</category>
      <category>airplay</category>
      <category>homepod</category>
      <category>linux</category>
    </item>
    <item>
      <title>Chrome extension 101 - implementing an extension</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Mon, 06 Jan 2025 14:27:25 +0000</pubDate>
      <link>https://dev.to/voodu/chrome-extension-101-implementing-an-extension-54pn</link>
      <guid>https://dev.to/voodu/chrome-extension-101-implementing-an-extension-54pn</guid>
      <description>&lt;p&gt;In the &lt;a href="https://dev.to/voodu/chrome-extension-101-environment-setup-4536"&gt;previous post&lt;/a&gt;, I showed you how to set up a Chromium extension project, so it supports TypeScript, autocompletion wherever possible and just works nicely as a starter. Now, I'll briefly show the implementation of my simple Page Audio extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Idea
&lt;/h3&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%2Fdp796el2ljti1u4zsjvu.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%2Fdp796el2ljti1u4zsjvu.png" alt="Lightbulb doodle" width="777" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What I wanted from my extension was very simple - when I go to a specific website, it should start playing predefined audio. Hard-coded website name and audio are completely fine.&lt;/p&gt;

&lt;p&gt;In a bit more detail, the audio should start playing when I open &lt;code&gt;www.example.com&lt;/code&gt;, stop when I switch to a different tab, and resume when I go back to &lt;code&gt;www.example.com&lt;/code&gt;. Also, if I have two (or more) tabs with &lt;code&gt;www.example.com&lt;/code&gt; opened and I switch between them, the audio should keep playing without restarting. In other words, audio should be played on the whole extension level, not individual tabs.&lt;/p&gt;

&lt;h3&gt;
  
  
  General technical approach
&lt;/h3&gt;

&lt;p&gt;In short, we need to create &lt;code&gt;HTMLAudioElement&lt;/code&gt; somewhere and play/pause it depending on the website in the current tab.&lt;/p&gt;

&lt;p&gt;It is doable with service worker and content scripts - we could have a content script creating an &lt;code&gt;HTMLAudioElement&lt;/code&gt; element on every page and use a service worker to coordinate the playback. When the tab loses focus, it passes the current media time frame to the service worker and when another tab with a matching URL gains focus, it asks the service worker for the time frame and resumes the playback from there.&lt;/p&gt;

&lt;p&gt;However, I think this approach is a bit convoluted and might be prone to errors. It would be much nicer if we could have only one &lt;code&gt;HTMLAudioElement&lt;/code&gt; element and play/pause it globally, not from individual tabs. Luckily, there's an interesting API that will greatly help us - &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/offscreen" rel="noopener noreferrer"&gt;offscreen API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Offscreen API lets the extension create one invisible HTML document. Using it, we'll have a place to keep our &lt;code&gt;HTMLAudioElement&lt;/code&gt; and just play/pause it when needed. Bear in mind that service worker still can't do any DOM operations, so we'll need some helper script on our offscreen document to receive service worker messages and adequately control the player.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&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%2F33p6w2vs3fb5q25aabk0.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%2F33p6w2vs3fb5q25aabk0.png" alt="Excited to code meme" width="323" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Needed permissions in &lt;code&gt;manifest.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;My extension needs two entries in the &lt;code&gt;permissions&lt;/code&gt; array:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tabs&lt;/code&gt; - it needs to know when the user is switching and/or updating tabs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;offscreen&lt;/code&gt; - it needs ability to create offscreen document to play the audio from there&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you open extension details in the browser, you'll see permissions described as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read your browsing history&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It might look a bit scary, but that's what adding &lt;code&gt;tabs&lt;/code&gt; permission causes. Unfortunately, I wasn't able to figure out a different approach with less concerning permissions. The other ideas I had were resulting in even scarier permission sets 😅 In &lt;a href="https://groups.google.com/a/chromium.org/g/chromium-extensions/c/au_qakYSWkk" rel="noopener noreferrer"&gt;this thread&lt;/a&gt; you can read why &lt;code&gt;tabs&lt;/code&gt; permission causes that entry.&lt;/p&gt;

&lt;h3&gt;
  
  
  Managing offscreen documents
&lt;/h3&gt;

&lt;p&gt;As I've mentioned, I would like to have only one &lt;code&gt;HTMLAudioElement&lt;/code&gt; and play the audio from it. To make it tab-independent, I'll use &lt;code&gt;offscreen&lt;/code&gt; API to create a document where it will be kept and controlled by messages from the service worker.&lt;/p&gt;

&lt;p&gt;I feel like object-oriented programming, so here's &lt;code&gt;OffscreenDoc&lt;/code&gt; class helping with offscreen document management. In essence, it just creates the offscreen document if it's not created yet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/offscreen-doc.ts&lt;/span&gt;
&lt;span class="cm"&gt;/**
 * Static class to manage the offscreen document
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OffscreenDoc&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;isCreating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// private constructor to prevent instantiation&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Sets up the offscreen document if it doesn't exist
     * @param path - path to the offscreen document
     */&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isDocumentCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOffscreenDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createOffscreenDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCreating&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCreating&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCreating&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;offscreen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocument&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;reasons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AUDIO_PLAYBACK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="na"&gt;justification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Used to play audio independently from the opened tabs&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;await&lt;/span&gt; &lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCreating&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isCreating&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&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;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;isDocumentCreated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Check all windows controlled by the service worker to see if one&lt;/span&gt;
        &lt;span class="c1"&gt;// of them is the offscreen document with the given path&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offscreenUrl&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;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&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;existingContexts&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContexts&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;contextTypes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OFFSCREEN_DOCUMENT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="na"&gt;documentUrls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;offscreenUrl&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;existingContexts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the only &lt;code&gt;public&lt;/code&gt; method is &lt;code&gt;setup&lt;/code&gt; and it needs some &lt;code&gt;path&lt;/code&gt; when called. That's a path to an HTML document template that will be used to create our offscreen document. It's gonna be super simple in our case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- offscreen.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"dist/offscreen.js"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Literally, just one script tag. This script will be used to receive service worker messages, create &lt;code&gt;HTMLAudioElement&lt;/code&gt;, and play/pause the music. It also has &lt;code&gt;type="module"&lt;/code&gt; as I will &lt;code&gt;import&lt;/code&gt; something there.&lt;/p&gt;

&lt;p&gt;But to receive messages, we should probably send them first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Message interface
&lt;/h3&gt;

&lt;p&gt;There isn't any strict interface for messages. We just need to make sure they are JSON-serializable. However, I would like to be as type-safe as possible, so I defined a simple interface for messages passed in my extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/audio-message.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AudioMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/**
     * Command to be executed on the audio element.
     */&lt;/span&gt;
    &lt;span class="nl"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;play&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pause&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="cm"&gt;/**
     * Source of the audio file.
     */&lt;/span&gt;
    &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see in a moment that the &lt;code&gt;sendMessage&lt;/code&gt; method isn't that great fit for typing, but there's an easy workaround to still benefit from type safety there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending messages from the service worker
&lt;/h3&gt;

&lt;p&gt;The service worker is the "brain" of our extension, knows what and when is happening, and should send appropriate messages as needed. But when is it exactly?&lt;/p&gt;

&lt;p&gt;We should change the playback state in three situations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when a new tab is activated, so user simply changes from tab A to tab B,&lt;/li&gt;
&lt;li&gt;when the current tab is updated, so its URL has changed, or&lt;/li&gt;
&lt;li&gt;when a tab is closed - that's a bit tricky case, as it might happen without invoking any of the two above cases when the user closes the last incognito window while the audio is playing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All situations mean we might be on the website where we want the audio to play or that we've just closed/left it.&lt;/p&gt;

&lt;p&gt;Without further ado, here's the updated &lt;code&gt;ts/background.ts&lt;/code&gt; script reacting to the two events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/background.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AudioMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./audio-message.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./offscreen-doc.js&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;affectedPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/&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;defaultAudio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assets/audio.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Play audio when the tab with affectedPage is active&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;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onActivated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;activeInfo&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;toggleAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&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="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;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onUpdated&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changeInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;toggleAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&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="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onRemoved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tabs&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;activeTab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;tab&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;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;active&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;toggleAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeTab&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tabUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;OffscreenDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offscreen.html&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;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tabUrl&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;affectedPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;play&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pause&lt;/span&gt;&lt;span class="dl"&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;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultAudio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;AudioMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the &lt;code&gt;toggleAudio&lt;/code&gt; function is the most important here. First of all, it sets up the offscreen document. It's safe to call it multiple times, as it just does nothing if the document is already created. Then it decides if it should send &lt;code&gt;"play"&lt;/code&gt; or &lt;code&gt;"pause"&lt;/code&gt; command, depending on the URL of the current tab. Finally, it sends the message. As I've mentioned, &lt;code&gt;sendMessage&lt;/code&gt; doesn't have a generic variant (&lt;code&gt;sendMessage&amp;lt;T&amp;gt;&lt;/code&gt;) so it's non-trivial to specify the message type, but TS &lt;code&gt;satisfies&lt;/code&gt; helps with making sure that the object we are sending is of &lt;code&gt;AudioMessage&lt;/code&gt; type.&lt;/p&gt;

&lt;p&gt;Notice also the two constants at the top - here you specify what audio you want to play and at which website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Receiving the messages by offscreen document
&lt;/h3&gt;

&lt;p&gt;Finally, we are sending the messages, so now it's time to receive them and play some music 🎶&lt;/p&gt;

&lt;p&gt;To do this, we need to implement the script used by &lt;code&gt;offscreen.html&lt;/code&gt;. It's &lt;code&gt;dist/offscreen.js&lt;/code&gt;, so that's how corresponding &lt;code&gt;ts/offscreen.ts&lt;/code&gt; looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/offscreen.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AudioMessage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./audio-message.js&lt;/span&gt;&lt;span class="dl"&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;audio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLAudioElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Listen for messages from the extension&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;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AudioMessage&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;audio&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;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;]();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In short, if we haven't created &lt;code&gt;HTMLAudioElement&lt;/code&gt; we're doing that using the provided source and then we're playing/pausing it. Returning &lt;code&gt;undefined&lt;/code&gt; is needed for typing purposes. If you're interested in the meaning of the different &lt;code&gt;return&lt;/code&gt; values, check &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/messaging" rel="noopener noreferrer"&gt;the docs&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&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%2Fq4bshz1kccpzboskoexu.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%2Fq4bshz1kccpzboskoexu.png" alt="Completed doodle" width="777" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it out! Go to &lt;code&gt;www.example.com&lt;/code&gt; (or whatever website you've set) and see if the audio is playing. Try switching tabs back and forth and verify if it correctly stops and resumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Take into account that if you pause music for more than 30 seconds, it will be restarted, as the service worker will be terminated by the browser!&lt;/strong&gt; Here are some &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle#idle-shutdown" rel="noopener noreferrer"&gt;docs&lt;/a&gt; about that.&lt;/p&gt;

&lt;p&gt;To summarize what we did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we updated our &lt;code&gt;manifest.json&lt;/code&gt; with the required permissions to create an offscreen document and monitor activity on tabs&lt;/li&gt;
&lt;li&gt;we made the service worker observe activity on tabs and send adequate commands to the script living in the offscreen document&lt;/li&gt;
&lt;li&gt;we started playing audio via a script that receives messages from the service worker and controls the DOM of the offscreen document&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I hope it was clear and easy to follow! There's quite a natural progression of this extension - letting the user specify different websites and assign different audio to each of them. Hopefully, I'll add that when I have some time and write another post describing my approach.&lt;/p&gt;

&lt;p&gt;For now, thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Chrome extension 101 - environment setup</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Tue, 31 Dec 2024 15:52:36 +0000</pubDate>
      <link>https://dev.to/voodu/chrome-extension-101-environment-setup-4536</link>
      <guid>https://dev.to/voodu/chrome-extension-101-environment-setup-4536</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I want some goofy functionality in my browser. Maybe I can add it with a simple extension? It doesn't exist, but writing it myself should be easy, right?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's what I thought a couple of days ago. While I wasn't &lt;em&gt;completely&lt;/em&gt; wrong, some parts of the development process were a bit more time-consuming than I expected. I won't say difficult, but rather hard to figure out using available documentation. While API documentation, core concepts, etc. are described quite nicely on &lt;a href="https://developer.chrome.com/docs/extensions/get-started" rel="noopener noreferrer"&gt;developer.chrome.com&lt;/a&gt;, I wanted a specific developer experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript with proper typing of &lt;code&gt;chrome&lt;/code&gt; namespace&lt;/li&gt;
&lt;li&gt;Splitting the code into multiple files and &lt;code&gt;import&lt;/code&gt;/&lt;code&gt;export&lt;/code&gt; what was necessary&lt;/li&gt;
&lt;li&gt;Debugging my code with simple &lt;code&gt;console.log&lt;/code&gt; and/or &lt;code&gt;debugger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Autocompletion in my &lt;code&gt;manifest.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Simple setup, without any bundlers and half of the Internet in my &lt;code&gt;node_modules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Simple way of updating and testing the extension in the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a better or worse way, I managed to set things up as I wanted. In this post, I'll briefly explain general extension concepts and show you how I've set up my development environment. In the next post or two I'll focus on the implementation details of my simple page-audio extension.&lt;/p&gt;

&lt;p&gt;TLDR:&lt;br&gt;
If you just want the code, here's the boilerplate repo:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Voodu" rel="noopener noreferrer"&gt;
        Voodu
      &lt;/a&gt; / &lt;a href="https://github.com/Voodu/chromium-extension-boilerplate" rel="noopener noreferrer"&gt;
        chromium-extension-boilerplate
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Chromium extension boilerplate&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;This repository aims at being a starting point for developing a chromium extension.&lt;/p&gt;
&lt;p&gt;It's as minimalistic as possible, but comes with pre-configured:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;autocompletion for &lt;code&gt;manifest.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;TypeScript transpilation from &lt;code&gt;ts&lt;/code&gt; folder to &lt;code&gt;dist&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;types for &lt;code&gt;chrome&lt;/code&gt; namespace&lt;/li&gt;
&lt;li&gt;properly working &lt;code&gt;export&lt;/code&gt;ing and &lt;code&gt;import&lt;/code&gt;ing (with VS Code workspace setting for correct auto import format)&lt;/li&gt;
&lt;li&gt;example &lt;code&gt;manifest.json&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Related blog post: &lt;a href="https://dev.to/voodu/chrome-extension-101-environment-setup-4536" rel="nofollow"&gt;https://dev.to/voodu/chrome-extension-101-environment-setup-4536&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Happy coding!&lt;/p&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Voodu/chromium-extension-boilerplate" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;ℹ️ &lt;em&gt;I use Windows 11, MS Edge, VS Code, and npm everywhere below&lt;/em&gt; ℹ️&lt;/p&gt;




&lt;h2&gt;
  
  
  Brief intro to extensions
&lt;/h2&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%2F89reeyx1y4cra561h62v.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%2F89reeyx1y4cra561h62v.png" alt="Intro doodle" width="777" height="143"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's start with a crash course on general extension concepts.&lt;/p&gt;

&lt;p&gt;Every extension has a &lt;code&gt;manifest.json&lt;/code&gt; file that defines its name, version, required permissions, and used files. Extensions can provide functionality in several different ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;via &lt;a href="https://developer.chrome.com/docs/extensions/develop/ui/add-popup" rel="noopener noreferrer"&gt;popup&lt;/a&gt; - extension popup is this small window that opens when you click the extension icon in the extension bar,&lt;/li&gt;
&lt;li&gt;via &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts" rel="noopener noreferrer"&gt;content scripts&lt;/a&gt; - scripts that are injected directly into websites and have DOM access,&lt;/li&gt;
&lt;li&gt;via &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/service-workers" rel="noopener noreferrer"&gt;background (service worker) scripts&lt;/a&gt; - scripts run in a separate context, independent from opened websites&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are other ways, but I'll stick to these three in this guide.&lt;/p&gt;

&lt;p&gt;Another important concept is &lt;strong&gt;messaging&lt;/strong&gt;. Usually, we need to combine the above methods, as all of them have different limitations. For example, background scripts don't depend on opened tabs and can be more useful for persisting state, but can't access the DOM of any website. Therefore, we might need to get some extension-wide data from the background script, pass it using a message to a content script, and modify the website from there.&lt;/p&gt;

&lt;p&gt;It can also be useful to understand some basics about permissions. In short, some APIs won't work as expected if &lt;code&gt;manifest.json&lt;/code&gt; doesn't specify the correct permissions. For example, if we don't specify &lt;code&gt;"tabs"&lt;/code&gt; permission, objects returned from the &lt;code&gt;tabs&lt;/code&gt; API won't have a &lt;code&gt;url&lt;/code&gt; field. On the other hand, we shouldn't ask for too many permissions - if the extension is going to be public, users might be concerned about giving access to too many things.&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating a simple extension
&lt;/h2&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%2Frx9zw6svry688qfy6toh.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%2Frx9zw6svry688qfy6toh.png" alt="Hello, World doodle" width="777" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Inspired by &lt;a href="https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world" rel="noopener noreferrer"&gt;https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Let's start with understanding the core concepts of our development workflow using an extremely simple extension that just displays some text in a popup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Files
&lt;/h3&gt;

&lt;p&gt;First of all, we need a &lt;code&gt;manifest.json&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// manifest.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hello World"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Shows Hello World text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_popup"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hello.html"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"default_icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icon.png"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, and &lt;code&gt;manifest_version&lt;/code&gt; are probably self-explanatory. &lt;code&gt;action.default_popup&lt;/code&gt; is a path to an HTML file that will be rendered upon clicking the extension icon. &lt;code&gt;default_icon&lt;/code&gt; is a path to extension icon. Both paths are relative to &lt;code&gt;manifest.json&lt;/code&gt; location.&lt;/p&gt;

&lt;p&gt;Now, add &lt;code&gt;icon.png&lt;/code&gt; (for example, &lt;a href="https://developer.chrome.com/static/docs/extensions/get-started/tutorial/hello-world/image/icon.png" rel="noopener noreferrer"&gt;this one&lt;/a&gt;) and &lt;code&gt;hello.html&lt;/code&gt; files in the same directory as &lt;code&gt;manifest.json&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;hello.html&lt;/code&gt; can look like that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- hello.html --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Hello world&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your whole directory should look like that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── hello.html
├── icon.png
└── manifest.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Activating the extension
&lt;/h3&gt;

&lt;p&gt;To activate your extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a&gt;edge://extensions/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;In the left sidebar, enable "Developer mode"

&lt;ul&gt;
&lt;li&gt;"Allow extensions from other stores" might also be needed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Above the extension list click "Load unpacked"&lt;/li&gt;
&lt;li&gt;Select the folder with your extension files&lt;/li&gt;
&lt;li&gt;Your extension should appear on the list and its icon in the extensions toolbar 🥳&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, after clicking the icon it will show a small popup with "Hello world" text.&lt;/p&gt;

&lt;p&gt;That covers the most important basics. Let's move to something more interesting.&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%2Fq0wxvndt7bkntg5rz3we.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%2Fq0wxvndt7bkntg5rz3we.png" alt="Level up doodle" width="777" height="164"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Page-Audio extension environment setup
&lt;/h2&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%2Fu9sepowqwvotbsn9a3ye.jpg" 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%2Fu9sepowqwvotbsn9a3ye.jpg" alt="Developer environment setup comic strip" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Autocomplete in &lt;code&gt;manifest.json&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;We'll start again with the &lt;code&gt;manifest.json&lt;/code&gt; and empty directory.&lt;/p&gt;

&lt;p&gt;It would be awesome to have autocomplete when writing the &lt;code&gt;manifest.json&lt;/code&gt; file, wouldn't it? Fortunately, it's a well-defined standard and has a JSON schema at &lt;a href="https://json.schemastore.org/chrome-manifest" rel="noopener noreferrer"&gt;https://json.schemastore.org/chrome-manifest&lt;/a&gt;. We just need it under the "$schema" key at the beginning of &lt;code&gt;manifest.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// manifest.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json.schemastore.org/chrome-manifest"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and VS Code instantly starts helping us by suggesting field names and showing warnings if mandatory fields are missing. Awesome!🔥&lt;/p&gt;

&lt;p&gt;To have something working for testing our setup, use &lt;code&gt;manifest.json&lt;/code&gt; looking this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// manifest.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json.schemastore.org/chrome-manifest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Page Audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"manifest_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icons"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"16"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/logo16x16.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"32"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/logo32x32.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"48"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/logo48x48.png"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"128"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"icons/logo128x128.png"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"service_worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/background.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"module"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;icons&lt;/code&gt; - it's just a different way of specifying extension icons&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;background&lt;/code&gt; section - specifies the path with the service worker JS file and its type; it's &lt;code&gt;module&lt;/code&gt; as the code will use &lt;code&gt;export&lt;/code&gt; and &lt;code&gt;import&lt;/code&gt; later on&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Using TypeScript... well, requires TypeScript. If you don't have it installed, start with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Basic config
&lt;/h4&gt;

&lt;p&gt;To have things organized, but not too complicated, I'll keep &lt;code&gt;.ts&lt;/code&gt; source files in the &lt;code&gt;ts&lt;/code&gt; directory. They will be taken from there by the transpiler and put in the &lt;code&gt;dist&lt;/code&gt; directory as &lt;code&gt;.js&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;This is described by the following &lt;code&gt;.tsconfig&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .tsconfig&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The most important bits are &lt;code&gt;compiler.rootDir&lt;/code&gt; and &lt;code&gt;compiler.outDir&lt;/code&gt;. The other fields can have different values or be completely removed (at least some of them).&lt;/p&gt;

&lt;p&gt;That's the basic configuration - placing some files in the &lt;code&gt;ts&lt;/code&gt; directory and running &lt;code&gt;tsc&lt;/code&gt; in the root directory will create a corresponding &lt;code&gt;.js&lt;/code&gt; file in &lt;code&gt;dist&lt;/code&gt;. However, we're missing one important part - types for the &lt;code&gt;chrome&lt;/code&gt; namespace that we'll be using. The simplest solution is to add them via npm.&lt;/p&gt;

&lt;h4&gt;
  
  
  Adding &lt;code&gt;chrome&lt;/code&gt; types
&lt;/h4&gt;

&lt;p&gt;Create an empty &lt;code&gt;package.json&lt;/code&gt;, just with the brackets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in the command line run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i -D chrome-types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also add &lt;code&gt;scripts&lt;/code&gt; to run &lt;code&gt;tsc&lt;/code&gt; build and in the watch mode. Final &lt;code&gt;package.json&lt;/code&gt; should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// package.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"watch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tsc -w"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"chrome-types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^0.1.327"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ℹ️ &lt;em&gt;&lt;code&gt;chrome-types&lt;/code&gt; version might be higher in your case.&lt;/em&gt; ℹ️&lt;/p&gt;

&lt;p&gt;After adding the types, we need to let TypeScript know about them. To do this, simply update &lt;code&gt;.tsconfig.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .tsconfig.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"module"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ES6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"outDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"rootDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"strict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"chrome-types"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Add this line!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To test if our setup works correctly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;In the &lt;code&gt;ts&lt;/code&gt; folder, create &lt;code&gt;background.ts&lt;/code&gt; file with the following content&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/background.ts&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;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onInstalled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&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="s1"&gt;Service Worker Installed&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;/li&gt;
&lt;li&gt;
&lt;p&gt;In the command line, run&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm run watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Verify if the &lt;code&gt;dist&lt;/code&gt; directory was created and &lt;code&gt;background.js&lt;/code&gt; file appeared there&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Change something in the &lt;code&gt;console.log&lt;/code&gt; string in &lt;code&gt;ts/background.ts&lt;/code&gt; file and save it&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Check if it automatically updated &lt;code&gt;dist/background.js&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If that works, awesome! We have nearly everything set up 🎉&lt;/p&gt;

&lt;p&gt;You can also verify if your directory structure looks similar to that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── dist
│   └── background.js
├── icons
│   ├── logo128x128.png
│   ├── logo16x16.png
│   ├── logo32x32.png
│   └── logo48x48.png
├── manifest.json
├── node_modules
│   └── chrome-types
│       └── ... some stuff inside ...
├── package-lock.json
├── package.json
├── ts
│   └── background.ts
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;export&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;As I've mentioned, I would like to split the code into smaller files. To do this, &lt;code&gt;export&lt;/code&gt;ing and &lt;code&gt;import&lt;/code&gt;ing must work correctly.&lt;/p&gt;

&lt;p&gt;One step in that direction was specifying our &lt;code&gt;service_worker&lt;/code&gt; in &lt;code&gt;manifest.json&lt;/code&gt; as &lt;code&gt;"type": "module"&lt;/code&gt;. However, there's one difference between TypeScript and JavaScript when working with modules - while TypeScript doesn't need file extensions when importing, JavaScript does. So, for example, this import:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;something&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./fileWithFunctions&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;will work in TS, but JS needs&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;something&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./fileWithFunctions.js&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;It's also important to understand, that TS transpiler &lt;strong&gt;does nothing&lt;/strong&gt; to the import paths. And it's "smart" enough to understand that when importing from &lt;code&gt;file.js&lt;/code&gt; it should also look for &lt;code&gt;file.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Combining all of that, TS will also be happy with JS-style import and will use the corresponding TS file when importing from &lt;code&gt;file.js&lt;/code&gt;. What &lt;em&gt;we&lt;/em&gt; need to do is make sure that all imports in TS files have a &lt;code&gt;.js&lt;/code&gt; extension. To automate it in VS Code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;CTRL + ,&lt;/code&gt; to open settings&lt;/li&gt;
&lt;li&gt;Switch to "Workspace" tab&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;typescript.preferences.importModuleSpecifierEnding&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Set it to ".js / .ts" option&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, whenever you auto import using VS Code, it will add &lt;code&gt;.js&lt;/code&gt; to the filename 🧠&lt;/p&gt;

&lt;p&gt;To test if things work correctly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create &lt;code&gt;ts/hello.ts&lt;/code&gt; file with the following content&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/hello.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hello&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="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In &lt;code&gt;ts/background.ts&lt;/code&gt; remove the current &lt;code&gt;console.log&lt;/code&gt; line and start typing "hello"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VS Code should autocomplete it and add the correct import after you accept the suggestion with Tab&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In the end, the file should look like this:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ts/background.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hello&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./hello.js&lt;/span&gt;&lt;span class="dl"&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;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onInstalled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&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="nf"&gt;hello&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;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note that import ends with the &lt;code&gt;.js&lt;/code&gt; extension. If you check &lt;code&gt;dist/background.js&lt;/code&gt; the extension is there as well and that's what makes everything work correctly.&lt;/p&gt;

&lt;p&gt;To make sure we are at the same stage, you can compare the directory structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.
├── .vscode
│   └── settings.json
├── dist
│   ├── background.js
│   └── hello.js
├── icons
│   └── ... icons ...
├── manifest.json
├── node_modules
│   └── chrome-types
│       └── ... some stuff inside ...
├── package-lock.json
├── package.json
├── ts
│   ├── background.ts
│   └── hello.ts
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dev Tools for service worker
&lt;/h3&gt;

&lt;p&gt;Okay, we have a decent development experience. We've also added some &lt;code&gt;console.log&lt;/code&gt; calls... but where to find them now?&lt;/p&gt;

&lt;p&gt;If you add &lt;code&gt;console.log&lt;/code&gt; inside a content script, you can simply open Dev Tools and they will be there, as content scripts work in the same context as the page they are injected into. However,  &lt;code&gt;console.log&lt;/code&gt;s from background scripts are hidden a bit more.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a&gt;edge://extensions/&lt;/a&gt; and load your extension if you haven't done that yet&lt;/li&gt;
&lt;li&gt;Find your extension on the list&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click "service worker" link in "Inspect views" line:&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%2F9k6ig79dzdbfogmj2cjo.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%2F9k6ig79dzdbfogmj2cjo.png" alt="Marked location of service worker link" width="298" height="160"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A new Dev Tools window should open and you'll see logs from the service worker there&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if you don't see the logs, click "Reload" below the "Inspect views"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The three links at the bottom of the tile are also very important&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Reload" - refreshes the whole extension, including changes to &lt;code&gt;manifest.json&lt;/code&gt;; checkout this &lt;a href="https://developer.chrome.com/docs/extensions/get-started/tutorial/hello-world#when_to_reload_the_extension" rel="noopener noreferrer"&gt;table&lt;/a&gt; to understand when reloading might be needed&lt;/li&gt;
&lt;li&gt;"Remove" - deletes the extension&lt;/li&gt;
&lt;li&gt;"Details" - shows more information about the extension, for example, its permissions&lt;/li&gt;
&lt;li&gt;(optional) "Errors" - if there are errors when installing the service worker, this link will appear and take you to the list of errors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Phew. That took a moment, but, finally, our environment is set up nicely. From now on, we'll just have to&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;npm run watch&lt;/code&gt; (if you stopped it)&lt;/li&gt;
&lt;li&gt;Write our code in &lt;code&gt;ts&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;(Optionally) Reload the extension from the extensions tab&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And our extension will be automatically updated! ⚙️&lt;/p&gt;

&lt;p&gt;&lt;small&gt;If you have an idea how to also "Reload" automatically (w/o elaborate hacking), let me know in the comments&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&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%2Fbgwaypou3etnz5l89n8f.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%2Fbgwaypou3etnz5l89n8f.png" alt="Summary doodle" width="777" height="164"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We have our environment ready! &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Autocomplete works in &lt;code&gt;manifest.json&lt;/code&gt;, so we don't have to guess what are the correct values&lt;/li&gt;
&lt;li&gt;TypeScript helps us with using &lt;code&gt;chrome&lt;/code&gt; API correctly&lt;/li&gt;
&lt;li&gt;Code can be split into smaller, logical files&lt;/li&gt;
&lt;li&gt;The code we write in the &lt;code&gt;ts&lt;/code&gt; folder is updated automatically&lt;/li&gt;
&lt;li&gt;We know where to find Dev Tools for the service worker and content scripts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next part, I'll describe the implementation details of my small "Page audio" extension.&lt;/p&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>tutorial</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Blazor, dotnet CLI, VS Code &amp; Tailwind - how to set that up?</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Thu, 22 Aug 2024 18:33:45 +0000</pubDate>
      <link>https://dev.to/voodu/project-configuration-with-blazor-dotnet-cli-vs-code-20b6</link>
      <guid>https://dev.to/voodu/project-configuration-with-blazor-dotnet-cli-vs-code-20b6</guid>
      <description>&lt;p&gt;I'm a big fan of VS Code and love using it to write all types of apps I can. Also, I like working with C# and wanted to play a bit with Blazor recently. Combining both of these interests, I decided to write a small project in Blazor, but using VS Code instead of typical Visual Studio approach. It also forced me to learn dotnet CLI a bit more (not as much as I thought at the beginning, though 😄). In this post, I'll describe general project configuration which includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating a new Blazor Web App solution &amp;amp; project&lt;/li&gt;
&lt;li&gt;Adding a custom, "Shared", project to the solution&lt;/li&gt;
&lt;li&gt;VS Code setup&lt;/li&gt;
&lt;li&gt;Debugging in VS Code (including Hot Reload)&lt;/li&gt;
&lt;li&gt;Adding Bootstrap and Tailwind to the project&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Maybe, one day, I'll dive a bit more into the project implementation, but for now, it's gonna be just the environment setup 😉&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the project
&lt;/h2&gt;

&lt;p&gt;As mentioned, I wanted to learn a bit of dotnet CLI, so we'll create the solution and add needed projects using command line.&lt;/p&gt;

&lt;p&gt;The goal is to have a solution with the default Blazor Web App projects + additional "Shared" project that can contain, for example, business logic, classes common for backend and frontend, etc.&lt;/p&gt;

&lt;h3&gt;
  
  
  Empty solution and Blazor Web App project
&lt;/h3&gt;

&lt;p&gt;To create an empty solution, run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet new sln -o MyCoolProject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet new blazor -int Auto --empty -n MyCoolProject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to create a Blazor Web App project.&lt;/p&gt;

&lt;p&gt;Quick explanation of the arguments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;blazor&lt;/code&gt; - creates a Blazor Web App&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-int Auto&lt;/code&gt; - sets &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#render-modes" rel="noopener noreferrer"&gt;interactivity mode&lt;/a&gt; of the Blazor project to Auto which allows the project to automatically switch between server-side and client-side rendering based on the context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--empty&lt;/code&gt; - project will be empty, without any predefined pages/components&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-n MyCoolProject&lt;/code&gt; - sets the name of the project&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  "Shared" class library
&lt;/h3&gt;

&lt;p&gt;The "Shared" project is a simple class library, so it can be created by running&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet new classlib -o MyCoolProject.Shared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connecting things - adding to solution and referencing
&lt;/h3&gt;

&lt;p&gt;We have all the projects, but not linked to the solution. Let's fix that with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet sln add .\MyCoolProject\MyCoolProject.csproj .\MyCoolProject.Client\MyCoolProject.Client.csproj .\MyCoolProject.Shared\MyCoolProject.Shared.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then reference "Shared" project in both other projects with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add .\MyCoolProject\MyCoolProject.csproj reference .\MyCoolProject.Shared\MyCoolProject.Shared.csproj
dotnet add .\MyCoolProject.Client\MyCoolProject.Client.csproj reference .\MyCoolProject.Shared\MyCoolProject.Shared.csproj
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's nearly done. However, to make the Shared code actually visible in the other ones, we also need to update global imports. To do that, add&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@using MyCoolProject.Shared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;at the end of&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MyCoolProject\Components\_Imports.razor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MyCoolProject.Client\_Imports.razor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Done? Try&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and see if things work.&lt;/p&gt;

&lt;h2&gt;
  
  
  VS Code setup
&lt;/h2&gt;

&lt;p&gt;For the previous steps we didn't even have to open VS Code. Time to change that.&lt;/p&gt;

&lt;p&gt;First - extensions. Probably the most important one is &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit" rel="noopener noreferrer"&gt;C# Dev Kit&lt;/a&gt;. You can read its description yourself, but, in essence, it makes VS Code more IDE-like for C# compared to being just a text editor. You might also look into &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.vscodeintellicode-csharp" rel="noopener noreferrer"&gt;IntelliCode for C# DevKit&lt;/a&gt; if you feel like normal code completion might be not enough.&lt;/p&gt;

&lt;p&gt;As we're already playing with the editor configuration, let's do one more thing - enable Hot Reload. As it's an experimental feature, it's not enabled by default. Simply, open VS Code settings (&lt;code&gt;CTRL + ,&lt;/code&gt;), search for "hot-reload" and check this setting:&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%2Fjt5mt5m2ruoibsqu1wlx.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%2Fjt5mt5m2ruoibsqu1wlx.png" alt="Screenshot of VS Code Hot Reload setting" width="573" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;At that point, we should be able to run and debug the code from VS Code - ideally, just press F5 and it should work. If not, click "Run and debug", select C#, and then something with "default configuration". At worst, restart VS Code (to be sure, really close and open it again, not just reload the window via the command palette).&lt;/p&gt;

&lt;p&gt;Now let's try actual debugging with a breakpoint. Due to the project config with Auto, remember about &lt;code&gt;@rendermode InteractiveAuto&lt;/code&gt; at the top of the &lt;code&gt;Home.razor&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@page "/"
@rendermode InteractiveAuto

&amp;lt;h3&amp;gt;Simple Counter&amp;lt;/h3&amp;gt;

&amp;lt;p&amp;gt;Current value: &amp;lt;strong&amp;gt;@counterValue&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;button @onclick="IncrementCounter"&amp;gt;Increment&amp;lt;/button&amp;gt;

@code {
    private int counterValue = 0;

    private void IncrementCounter()
    {
        counterValue++;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place a breakpoint in the &lt;code&gt;IncrementCounter&lt;/code&gt; method, run the app in debug mode, and, hopefully, it will be hit when you press the button to increase the counter.&lt;/p&gt;

&lt;p&gt;❗ If the counter doesn't work and you see an error in the DevTools console about the Home component not being found, move it from the &lt;code&gt;MyCoolProject/Components/Pages&lt;/code&gt; directory to &lt;code&gt;MyCoolProject.Client/Components&lt;/code&gt; ❗&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/project-structure?view=aspnetcore-8.0#:%7E:text=Components%20using%20the%20Interactive%20WebAssembly%20or%20Interactive%20Auto%20render%20modes%20must%20be%20located%20in%20the%20.Client%20project:~:text=Components%20using%20the%20Interactive%20WebAssembly%20or%20Interactive%20Auto%20render%20modes%20must%20be%20located%20in%20the%20.Client%20project." rel="noopener noreferrer"&gt;Here are some docs about it.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hot Reload
&lt;/h2&gt;

&lt;p&gt;There's not much to say about Hot Reload - after enabling the setting in VS Code, it should work.&lt;/p&gt;

&lt;p&gt;To test it, run the app, change something in the source file, save it, and refresh the page. Changes should be visible instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS library
&lt;/h2&gt;

&lt;p&gt;We are nearly done with the config! Let's sprinkle our project with some CSS library and we're good to go.&lt;/p&gt;

&lt;p&gt;I usually use Bootstrap and I started with setting up the project to use it. Then, I recalled I wanted to give Tailwind CSS a shot at some point, so that's the CSS library of choice here. As I, however, set up Bootstrap, here's a quick guide for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bootstrap
&lt;/h3&gt;

&lt;p&gt;Managing simple libraries in Blazor projects is the easiest when using LibMan. As this series is focused on doing stuff manually, I'll do the same with LibMan. Luckily, there's &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/client-side/libman/libman-cli?view=aspnetcore-8.0" rel="noopener noreferrer"&gt;a nice CLI tool for it&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Docs are super clear and to the point, so I won't write everything again here, but in short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;install LibMan globally with&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet tool install -g Microsoft.Web.LibraryManager.Cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;initialize it in the current project with&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libman init -p unpkg
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;in &lt;code&gt;.Client&lt;/code&gt; project folder (&lt;a href="https://stackoverflow.com/questions/68236284/how-to-setup-bootstrap-5-to-blazor-client-project#comment-125889872:~:text=i%20think%20i%20get%20it%20now.%20unpkg%20packages%20have%20the%20same%20directory%20strucutre%20with%20the%20%22dist%22%20folder%2C%20as%20visual%20studio%20templates.%20so%20the%20paths%20in%20the%20HTML%20or%20Page%2C%20don%27t%20have%20to%20be%20changed" rel="noopener noreferrer"&gt;why unpkg?&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;add Boostrap 5 with&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libman install bootstrap -d wwwroot/lib/bootstrap
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;If you're wondering why we didn't give any default destination - unfortunately, LibMan doesn't put a new library in the dedicated folder anyway 🤷&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you'll probably need Popper for some scripts to work, so add it with&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libman install "@popperjs/core" -d wwwroot/lib/popper
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;link CSS/HTML in &lt;code&gt;MyCoolProject/Components/Pages/App.razor&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    ...
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"lib/bootstrap/dist/css/bootstrap.min.css"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    ...
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    ...
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"lib/popper/dist/umd/popper.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"lib/bootstrap/dist/js/bootstrap.min.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;that should make most of the Bootstrap stuff work. You will still have problems with, for example, tooltips, though. Then check this &lt;a href="https://stackoverflow.com/a/64308003/9375075" rel="noopener noreferrer"&gt;SO answer&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I haven't checked the whole integration thoroughly, so treat the above steps just as a starter 😊&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tailwind
&lt;/h3&gt;

&lt;p&gt;Tailwind can be summarized as a set of utility CSS classes you can use to style your HTML. They are similar to Bootstrap &lt;code&gt;p-4&lt;/code&gt;, &lt;code&gt;mb-3&lt;/code&gt;, &lt;code&gt;d-flex&lt;/code&gt; etc. but way more powerful. Another point worth noting from a configuration point of view is its build process - there is a Tailwind tool that scans your source files, looks for Tailwind CSS class names, and includes them in the final CSS file you link to your main HTML. That's it in terms of theory - let's actually add Tailwind to our Blazor project. I've followed &lt;a href="https://chrissainty.com/adding-tailwind-css-v3-to-a-blazor-app/#integrating-using-the-tailwind-cli-via-npm" rel="noopener noreferrer"&gt;Chris Sainty's blog post on that&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First things first, we'll need the mentioned Tailwind tool for creating the output CSS. Install it globally with&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; tailwindcss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we need a main config file for Tailwind. To create it, go to the root directory of the project and run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx tailwindcss init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then, adjust &lt;code&gt;content&lt;/code&gt; field in the created &lt;code&gt;tailwind.config.js&lt;/code&gt; to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./MyCoolProject/**/*.{razor,html}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./MyCoolProject.Client/**/*.{razor,html}&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;It tells Tailwind which files to watch for the CSS classes.&lt;/p&gt;

&lt;p&gt;Then we need the main CSS files to be processed by Tailwind. We'll also include default Tailwind classes and styles there. We can use the existing &lt;code&gt;MyCoolProject\wwwroot\app.css&lt;/code&gt; file for that. Just add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;components&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@tailwind&lt;/span&gt; &lt;span class="n"&gt;utilities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;at the top&lt;/strong&gt; and that should do the trick.&lt;/p&gt;

&lt;p&gt;Using CLI, we should now tell Tailwind which file to process and where to put the final CSS that we'll link in the HTML. Our input will be the mentioned &lt;code&gt;app.css&lt;/code&gt; file, and we'll output the final CSS to &lt;code&gt;MyCoolProject\wwwroot\app-compiled.css&lt;/code&gt;. The command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx tailwindcss -i .\MyCoolProject\wwwroot\app.css -o .\MyCoolProject\wwwroot\app-compiled.css --watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Due to the &lt;code&gt;--watch&lt;/code&gt; switch it also automatically rebuilds the output after any changes in the input file are detected.&lt;/p&gt;

&lt;p&gt;To finish linking Tailwind to out app, go to &lt;code&gt;MyCoolProject\Components\App.razor&lt;/code&gt; and update one of the &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags which contains reference to &lt;code&gt;app.css&lt;/code&gt; and &lt;strong&gt;change it&lt;/strong&gt; to &lt;code&gt;app-compiled.css&lt;/code&gt;, i.e.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"app-compiled.css"&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;To test if things work, you can open &lt;code&gt;MyCoolProject.Client\Components\Home.razor&lt;/code&gt; and add somewhere&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;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-bold underline"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Hello world!
&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then launch the app (F5).&lt;/p&gt;

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

&lt;p&gt;In the post, I've very briefly described how to set up a dev environment for developing Blazor Web App using VS Code. We created solution, projects and linked them properly using dotnet CLI. Then we added the most important extensions to VS Code and updated it settings to enable Hot Reload. To finish the configuration, we added CSS library (Tailwind).&lt;/p&gt;

&lt;p&gt;It's very much &lt;strong&gt;not&lt;/strong&gt; in-depth post, but I hope it gives a general idea of how to do things 😊 Blazor is quite a new thing for me, so comments and questions are more than welcome.&lt;/p&gt;

&lt;p&gt;Thanks for reading! ❤️&lt;/p&gt;

&lt;p&gt;&lt;small&gt;&lt;br&gt;
&lt;em&gt;PS&lt;/em&gt;&lt;br&gt;
&lt;em&gt;I've been writing this post over quite some time (motivation, time &amp;amp; stuff), so if something doesn't work anymore, let me know!&lt;/em&gt;&lt;br&gt;
&lt;/small&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>vscode</category>
      <category>blazor</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Handling theme change in MS Teams app with Blazor</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Tue, 06 Dec 2022 22:49:31 +0000</pubDate>
      <link>https://dev.to/voodu/handling-theme-change-in-ms-teams-app-with-blazor-a39</link>
      <guid>https://dev.to/voodu/handling-theme-change-in-ms-teams-app-with-blazor-a39</guid>
      <description>&lt;p&gt;Recently, I've been playing a bit with Blazor and Microsoft Teams apps. I had hard time finding clear explanation how to handle theme changes in MS Teams. As, after spending some time, I figured out the way, I'll share my approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The problem is very simple - when user changes their theme settings in Microsoft Teams, we want to react to that instantly; without app reload. In my case, I want to change &lt;code&gt;BaseLayerLuminance&lt;/code&gt; of &lt;code&gt;FluentDesignSystemProvider&lt;/code&gt; in &lt;code&gt;App.razor&lt;/code&gt;, so all the Fluent UI components have their look updated automatically.&lt;/p&gt;

&lt;p&gt;The tricky part is that we want to do that using Blazor.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript approach
&lt;/h2&gt;

&lt;p&gt;Solution with JavaScript is incredibly simple, found at the &lt;a href="https://learn.microsoft.com/en-us/microsoftteams/platform/tabs/how-to/access-teams-context?tabs=teamsjs-v2#handle-theme-change"&gt;bottom of the docs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You can register your app to be informed if the theme changes by calling&lt;/span&gt;
&lt;span class="nx"&gt;microsoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerOnThemeChangeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Blazor we also have something similar to &lt;code&gt;microsoftTeams&lt;/code&gt; object when we inject &lt;code&gt;@inject MicrosoftTeams MicrosoftTeams&lt;/code&gt; in the component, so that should be pretty simple, right?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blazor approach
&lt;/h2&gt;

&lt;p&gt;Turns out, &lt;code&gt;Interop.TeamsSDK.MicrosoftTeams&lt;/code&gt; class doesn't expose any &lt;code&gt;app&lt;/code&gt; or &lt;code&gt;registerOnThemeChangeHandler&lt;/code&gt;. After searching a bit it gets even better - there doesn't seem to be any C# SDK doing that. This means we are stuck with JavaScript and JS Interop.&lt;/p&gt;

&lt;p&gt;Conceptually, it's rather simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create .js file which has a function calling &lt;code&gt;microsoftTeams.app.registerOnThemeChangeHandler&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Import and call the function from &lt;code&gt;App.razor&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;On theme change, the JS function calls C# code&lt;/li&gt;
&lt;li&gt;Update the variable bound to &lt;code&gt;BaseLayerLuminance&lt;/code&gt; and rerender the component&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, there are some technicalities which make it a bit more complicated than it looks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting code
&lt;/h3&gt;

&lt;p&gt;Code at the beginning looks like that. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;_luminance&lt;/code&gt; field is used to set &lt;code&gt;BaseLayerLuminance&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;after the first render, the theme is grabbed from the context and used to determine &lt;code&gt;_luminance&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UpdateLuminance&lt;/code&gt; has this weird async implementation as it's required later by the JS interop
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;// App.razor

@inject MicrosoftTeams MicrosoftTeams

&lt;span class="nt"&gt;&amp;lt;FluentDesignSystemProvider&lt;/span&gt; &lt;span class="na"&gt;AccentBaseColor=&lt;/span&gt;&lt;span class="s"&gt;"#6264A7"&lt;/span&gt; &lt;span class="na"&gt;BaseLayerLuminance=&lt;/span&gt;&lt;span class="s"&gt;"_luminance"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Router&lt;/span&gt; &lt;span class="na"&gt;AppAssembly=&lt;/span&gt;&lt;span class="s"&gt;"@typeof(Program).Assembly"&lt;/span&gt; &lt;span class="na"&gt;PreferExactMatches=&lt;/span&gt;&lt;span class="s"&gt;"@true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Found&lt;/span&gt; &lt;span class="na"&gt;Context=&lt;/span&gt;&lt;span class="s"&gt;"routeData"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;RouteView&lt;/span&gt; &lt;span class="na"&gt;RouteData=&lt;/span&gt;&lt;span class="s"&gt;"@routeData"&lt;/span&gt; &lt;span class="na"&gt;DefaultLayout=&lt;/span&gt;&lt;span class="s"&gt;"@typeof(MainLayout)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Found&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;NotFound&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;LayoutView&lt;/span&gt; &lt;span class="na"&gt;Layout=&lt;/span&gt;&lt;span class="s"&gt;"@typeof(MainLayout)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Sorry, there's nothing at this address.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/LayoutView&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/NotFound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Router&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/FluentDesignSystemProvider&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@code&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_luminance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnAfterRenderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnAfterRenderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstRender&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="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTeamsContextAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Theme&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;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_luminance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"dark"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"contrast"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// it doesn't make sense, but let's pretend it does&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Calling JS from Blazor component
&lt;/h3&gt;

&lt;p&gt;That is described pretty accurately in the &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-7.0"&gt;docs&lt;/a&gt;. The examples usually show the functions attached to the &lt;code&gt;window&lt;/code&gt; object. I just prefer to have them exported from the &lt;code&gt;.js&lt;/code&gt; file. Thus, that's my &lt;code&gt;ThemeChange.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./wwwroot/js/ThemeChange.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;onTeamsThemeChange&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;microsoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerOnThemeChangeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&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="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Theme changed to:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;As I want to update the theme of the whole app, I'll be modifying &lt;code&gt;App.razor&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;First of all, it needs &lt;code&gt;IJSRuntime&lt;/code&gt; to be injected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@inject IJSRuntime JS
// Rest of the HTML/Code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in the &lt;code&gt;@code&lt;/code&gt; block, we declare &lt;code&gt;IJSObjectReference&lt;/code&gt; field to hold the loaded module and call the function inside it. We load all of that after the first render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_luminance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IJSObjectReference&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_module&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnAfterRenderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnAfterRenderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstRender&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="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTeamsContextAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;_module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;JS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IJSObjectReference&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"./js/ThemeChange.js"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeVoidAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"onTeamsThemeChange"&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;Now, after changing the theme in Teams settings, console should log which theme is currently selected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Calling C# from JS
&lt;/h3&gt;

&lt;p&gt;At this point, we just want to call &lt;code&gt;UpdateLuminance&lt;/code&gt; from inside the callback of our JS function. Details are described in &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-7.0#create-javascript-object-and-data-references-to-pass-to-net"&gt;the docs&lt;/a&gt;. We have to follow the instance method way, as we are calling non-static &lt;code&gt;StatusHasChanged&lt;/code&gt; method, so it can't be called from a static context.&lt;/p&gt;

&lt;p&gt;Let's start with updating JS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./wwwroot/js/ThemeChange.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;onTeamsThemeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dotNetHelper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;microsoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerOnThemeChangeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;dotNetHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invokeMethodAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;Details are described in the docs, but in short, Blazor is going to pass some object which will be used to call the C# method. The second argument to &lt;code&gt;invokeMethodAsync&lt;/code&gt; is just an argument to &lt;code&gt;UpdateLuminance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Code in &lt;code&gt;App.razor&lt;/code&gt; will require a bit more changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Starting a bit bacwards, but we will have to clean up after all the JS Interop fun, so declare that the component is disposable:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;@implements IDisposable
@inject IJSRuntime JS
@inject MicrosoftTeams MicrosoftTeams
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Then, we have to declare a field to hold reference to the &lt;code&gt;App&lt;/code&gt; in JavaScript:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_luminance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IJSObjectReference&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_module&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DotNetObjectReference&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;_objRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This reference is created in overriden &lt;code&gt;OnInitialized&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInitialized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_objRef&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DotNetObjectReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;As our JS function has a parameter now, we need to pass it:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTeamsContextAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;_module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;JS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IJSObjectReference&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"./js/ThemeChange.js"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeVoidAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"onTeamsThemeChange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_objRef&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;/li&gt;
&lt;li&gt;
&lt;p&gt;Minor but important thing - our &lt;code&gt;UpdateLuminance&lt;/code&gt; method has to be decorated with &lt;code&gt;JSInvokable&lt;/code&gt; attribute:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSInvokable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;To finish up, don't forget about disposing:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_objRef&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&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;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Et voilà! Now theme of your Fluent UI controls should change instantly after user changes their settings 🙂&lt;/p&gt;




&lt;p&gt;Full code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;// App.razor
@implements IDisposable
@inject IJSRuntime JS
@inject MicrosoftTeams MicrosoftTeams

&lt;span class="nt"&gt;&amp;lt;FluentDesignSystemProvider&lt;/span&gt; &lt;span class="na"&gt;AccentBaseColor=&lt;/span&gt;&lt;span class="s"&gt;"#6264A7"&lt;/span&gt; &lt;span class="na"&gt;BaseLayerLuminance=&lt;/span&gt;&lt;span class="s"&gt;"_luminance"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Router&lt;/span&gt; &lt;span class="na"&gt;AppAssembly=&lt;/span&gt;&lt;span class="s"&gt;"@typeof(Program).Assembly"&lt;/span&gt; &lt;span class="na"&gt;PreferExactMatches=&lt;/span&gt;&lt;span class="s"&gt;"@true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;Found&lt;/span&gt; &lt;span class="na"&gt;Context=&lt;/span&gt;&lt;span class="s"&gt;"routeData"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;RouteView&lt;/span&gt; &lt;span class="na"&gt;RouteData=&lt;/span&gt;&lt;span class="s"&gt;"@routeData"&lt;/span&gt; &lt;span class="na"&gt;DefaultLayout=&lt;/span&gt;&lt;span class="s"&gt;"@typeof(MainLayout)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/Found&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;NotFound&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;LayoutView&lt;/span&gt; &lt;span class="na"&gt;Layout=&lt;/span&gt;&lt;span class="s"&gt;"@typeof(MainLayout)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Sorry, there's nothing at this address.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/LayoutView&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/NotFound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/Router&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/FluentDesignSystemProvider&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;@code&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;_luminance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;IJSObjectReference&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_module&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;DotNetObjectReference&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;_objRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;OnInitialized&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_objRef&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DotNetObjectReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&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="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;OnAfterRenderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OnAfterRenderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;firstRender&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="n"&gt;firstRender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;MicrosoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTeamsContextAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Theme&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;_module&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;JS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IJSObjectReference&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"./js/ThemeChange.js"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeVoidAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"onTeamsThemeChange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_objRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;JSInvokable&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_luminance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"dark"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.0f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s"&gt;"contrast"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0.5f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;1.0f&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="nf"&gt;StateHasChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_objRef&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ./wwwroot/js/ThemeChange.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;onTeamsThemeChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dotNetHelper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;microsoftTeams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;registerOnThemeChangeHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;dotNetHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;invokeMethodAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UpdateLuminance&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;theme&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;



</description>
      <category>blazor</category>
      <category>teams</category>
      <category>razor</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Windows 11 - making taskbar like Win10</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Thu, 20 Oct 2022 20:45:25 +0000</pubDate>
      <link>https://dev.to/voodu/windows-11-making-taskbar-like-win10-1le5</link>
      <guid>https://dev.to/voodu/windows-11-making-taskbar-like-win10-1le5</guid>
      <description>&lt;p&gt;...or even like WinXP (at least in terms of functioning 😉)&lt;/p&gt;

&lt;p&gt;Looking at &lt;a href="https://aka.ms/AAd2l82"&gt;Feedback Hub&lt;/a&gt; and &lt;a href="https://techcommunity.microsoft.com/t5/windows-11/how-to-enable-never-combine-taskbar-buttons-windows-11/m-p/2824344"&gt;Tech Community&lt;/a&gt; shows how incredibly users hate Windows 11... or at least its taskbar. Among several issues with it, I definitely miss combining buttons the most. Although seems like &lt;a href="https://blogs.windows.com/windows-insider/2022/10/20/announcing-windows-11-insider-preview-build-22621-870-and-22623-870/"&gt;Microsoft is doing something with the taskbar&lt;/a&gt;, combining buttons is still not possible. Unless we know the proper tool. Let's see, how easily can we transform this&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bAvHC5Kp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u70ossmi8de152ifsyut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bAvHC5Kp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u70ossmi8de152ifsyut.png" alt="Default Windows 11 taskbar" width="880" height="104"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;into that&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BHiz5lLD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkm6dic0tb2zet4p0y0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BHiz5lLD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xkm6dic0tb2zet4p0y0u.png" alt="Customized Windows 11 taskbar" width="880" height="48"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Explorer Patcher
&lt;/h2&gt;

&lt;p&gt;As Windows 11 doesn't natively give any option to extensively modify the taskbar, we have to use a third-party tool - Explorer Patcher. To get it, just go to &lt;a href="https://github.com/valinet/ExplorerPatcher/releases"&gt;its GitHub repository releases&lt;/a&gt;, expand &lt;code&gt;Assets&lt;/code&gt; of the newest version and download &lt;code&gt;ep_setup.exe&lt;/code&gt;. Then just install using defaults, it doesn't have many options to choose from. Your screen should blink and File Explorer should restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proper settings
&lt;/h3&gt;

&lt;p&gt;To customize your taskbar using Explorer Patcher, right-click the taskbar and open &lt;code&gt;Properties&lt;/code&gt;. At the top of the right panel you want to choose &lt;code&gt;Taskbar style*: Windows 10&lt;/code&gt;. Then, at the bottom make sure that &lt;code&gt;Combine taskbar icons on primary/secondary taskbar&lt;/code&gt; is set to &lt;code&gt;Never combine (default)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MaFEkFcO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ltsh01rtehutye5cnyd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MaFEkFcO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7ltsh01rtehutye5cnyd.png" alt="Explorer Patcher settings" width="635" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After that, in the bottom left click &lt;code&gt;Restart File Explorer&lt;/code&gt;. Again, screen will blink and you should see a completely different taskbar; the one you know from Windows 10.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taskbar like Windows XP
&lt;/h2&gt;

&lt;p&gt;Personally, I really liked the behaviour of the taskbar in Windows XP - icons to launch the applications on the left, tabs with currently open windows on their right. Let's do it in Windows 11.&lt;/p&gt;

&lt;p&gt;Before anything, as we're about to move things around, make sure your taskbar is unlocked. Right-click on it and check if &lt;code&gt;Lock all taskbars&lt;/code&gt; doesn't have a checkmark next to it. If it does, select it to "untick".&lt;/p&gt;

&lt;p&gt;Start with some cleanup and unpin all the current icon shortucts from the taskbar. It should be empty. Something like this on the screnshot&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Zqyfz_F2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6k1au0pmmi6qdft0z0po.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Zqyfz_F2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6k1au0pmmi6qdft0z0po.png" alt="Empty taskbar" width="880" height="24"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Toolbar with custom shortcuts
&lt;/h3&gt;

&lt;p&gt;Then, we will need a folder with shortcuts which should appear as the app launcher icons on the taskbar. Create the folder wherever you want and put some shortcuts in it. I called mine... &lt;code&gt;Taskbar&lt;/code&gt; (sic!) and placed 5 shortcuts in there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--blvOwN7n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qfuzb89j4jtor3p00bt1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--blvOwN7n--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qfuzb89j4jtor3p00bt1.png" alt="Toolbar/Taskbar folder" width="880" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we'll connect the folder with the taskbar. To do this, right-click on the taskbar, expand &lt;code&gt;Toolbars&lt;/code&gt; and click &lt;code&gt;New toolbar&lt;/code&gt; at the bottom. Now just select the folder with your shortcuts. Tadaaa!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8WX_VVrH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ttijzrh887xdy91mtch.GIF" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8WX_VVrH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ttijzrh887xdy91mtch.GIF" alt="Selecting new toolbar folder" width="880" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tadaaa...? Not yet. Things are not where they should be. Also, this "Taskbar" name is not really necessary. And if you drag the handle to the left, you'll see that icons don't look as you probably want.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing the toolbar
&lt;/h3&gt;

&lt;p&gt;Let's solve those problems one by one:&lt;/p&gt;

&lt;h4&gt;
  
  
  Title
&lt;/h4&gt;

&lt;p&gt;To remove the title, simply right-click it and uncheck &lt;code&gt;Show title&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Shortcut names
&lt;/h4&gt;

&lt;p&gt;Similarly, to hide those long names, right click somewhere on the toolbar and uncheck &lt;code&gt;Show Text&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Icon size
&lt;/h4&gt;

&lt;p&gt;To make things aesthetically fine, icons on tabs with open apps should have the same (or at least similar) size as the launcher icons on the toolbar. To adjust the first, use &lt;code&gt;Taskbar icon size&lt;/code&gt; option in Explorer Patcher properties and set it to &lt;code&gt;Large&lt;/code&gt; (it should be fine by default). To adjust toolbar icon size, right-click on it, expand &lt;code&gt;View&lt;/code&gt; (at the top of the context menu) and select &lt;code&gt;Large icons&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Position
&lt;/h4&gt;

&lt;p&gt;That's the most complex thing and will require advanced programming skills combined with the extensive knowledge of the registry. &lt;br&gt;
Ok, just kidding 🙂 It's not that hard, but probably the least intuitive. To move the toolbar to the left, so it appears on the left side of the open apps, you just have to brute-force it 😉 Grab the handle and pull it to the left as much as possible, until the app icons "jump" to the right:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--kcKQHVCs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7vf046jqi9cx6qf988wk.GIF" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kcKQHVCs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7vf046jqi9cx6qf988wk.GIF" alt="Pulling the toolbar to the left" width="880" height="45"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To finish up, just adjust width of the toolbar and lock all the taskbars.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus - shortcut to File Explorer
&lt;/h2&gt;

&lt;p&gt;There must be some simple way to create a shortcut to File Explorer, but I couldn't find it at all. If you also need it, just&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-click anywhere on the desktop and select &lt;code&gt;New &amp;gt; Shortcut&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;As the location type &lt;code&gt;C:\Windows\explorer.exe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Name it as "File Explorer"&lt;/li&gt;
&lt;li&gt;Voila, after double clicking the shortcut it should open the File Explorer&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;I hope you found it useful and everything worked as it should. If not, let me know 🙂&lt;br&gt;
To be honest, this whole process is way more cumbersome than it should be. Also, I'd prefer to have the small icons in the toolbar, but I couldn't find a way to make the icons of the open apps the same size. If you know any way of achieving that, I'd be happy to hear about it 🙂&lt;br&gt;
Thank you for reading! ❤️&lt;/p&gt;

</description>
      <category>windows</category>
      <category>taskbar</category>
      <category>win11</category>
      <category>toolbar</category>
    </item>
    <item>
      <title>Vue 3, PWA &amp; service worker</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Sun, 03 Jan 2021 17:07:45 +0000</pubDate>
      <link>https://dev.to/voodu/vue-3-pwa-service-worker-12di</link>
      <guid>https://dev.to/voodu/vue-3-pwa-service-worker-12di</guid>
      <description>&lt;p&gt;Recently I started playing with Vue. Of course, starting with "hello world" calculator with well-documented Vue 2 was not an option, so I decided to go with simple PWA in Vue 3. Setting up a project wasn't as easy as it may appear, so I'll describe it here for anyone interested (and future reference for myself).&lt;/p&gt;

&lt;p&gt;I'll describe everything from (nearly) scratch, so hopefully it'll be of use for complete beginners. I &lt;strong&gt;will not&lt;/strong&gt; explain philosophy of Vue, PWA or service workers - it will be just about setting those things up.&lt;/p&gt;

&lt;p&gt;I'm using Win10, so I'll describe the process from this PoV (however, it matters only for Node installation).&lt;/p&gt;

&lt;h2&gt;
  
  
  Node, npm and Vue
&lt;/h2&gt;

&lt;p&gt;As with all JS projects, it's simpler to do it with Node &amp;amp; npm.&lt;/p&gt;

&lt;p&gt;If you don't already have them, I recommend installing Node with &lt;a href="https://github.com/coreybutler/nvm-windows" rel="noopener noreferrer"&gt;nvm&lt;/a&gt;. Probably the easiest way is just going &lt;a href="https://github.com/coreybutler/nvm-windows/releases" rel="noopener noreferrer"&gt;here&lt;/a&gt;, downloading latest &lt;em&gt;nvm-setup.zip&lt;/em&gt;, extracting and running the installer. After that, you should be able to use &lt;code&gt;nvm&lt;/code&gt; in your command prompt. If you want to install latest &lt;strong&gt;stable&lt;/strong&gt; version just go with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm &lt;span class="nb"&gt;install &lt;/span&gt;latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For some particular version you can execute&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm &lt;span class="nb"&gt;install &lt;/span&gt;15.4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, remember to &lt;code&gt;use&lt;/code&gt; it!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm use 15.4.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Node, npm should be automatically installed as well. For me, Node version is 15.4.0 and npm is 7.3.0.&lt;/p&gt;

&lt;p&gt;To make our lives easier, there's also &lt;a href="https://cli.vuejs.org/guide/" rel="noopener noreferrer"&gt;Vue CLI&lt;/a&gt; which helps with setting up the project. Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @vue/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will allow you to use &lt;code&gt;vue&lt;/code&gt; command from your terminal. For me, &lt;code&gt;vue --version&lt;/code&gt; returns &lt;code&gt;@vue/cli 4.5.9&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Now, we can start with our mini-project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating project
&lt;/h2&gt;

&lt;p&gt;Creating a new project with Vue CLI is extremely simple. Just go with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vue create our-app-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then using arrows just select the options. I picked:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Manually select features&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;and then selected with spacebar &lt;code&gt;Progressive Web App (PWA) support&lt;/code&gt;. Press Enter to continue, then choose Vue version to &lt;code&gt;3.x&lt;/code&gt;, &lt;code&gt;ESLint with error prevention only&lt;/code&gt;, &lt;code&gt;Lint on save&lt;/code&gt;, &lt;code&gt;In dedicated config files&lt;/code&gt;, type &lt;code&gt;n&lt;/code&gt; and press Enter to generate the project (it will take 1-2 minutes).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Of course you can choose different options. Only PWA support is necessary&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Generated project is runnable out of the box. First of all, remember to navigate to the created project folder, then run development server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;our-app-name
npm run serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output should give you addresses where you can access your generated app. For me it's &lt;code&gt;http://localhost:8080/&lt;/code&gt; (if you want to stop it, just &lt;code&gt;CTRL+C&lt;/code&gt; it)&lt;/p&gt;

&lt;p&gt;Note that currently service worker is not working - if you go to &lt;em&gt;Application &amp;gt; Service worker&lt;/em&gt; in DevTools you won't see it. Generated project makes service worker active only in production build. Let's check it.&lt;/p&gt;

&lt;p&gt;To create a production build, run&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Give it some time and it will create &lt;code&gt;dist&lt;/code&gt; directory in your project folder. Now you need to host it somewhere. I'd recommend &lt;a href="https://chrome.google.com/webstore/detail/web-server-for-chrome/ofhbbkphhbklhfoeikjpcbhemlocgigb?hl=en" rel="noopener noreferrer"&gt;Web Server for Chrome&lt;/a&gt;, as it's very easy to use and works fine (I tried also Python simple http server, but it didn't work correctly for me, so watch out for that). Just select your &lt;em&gt;dist&lt;/em&gt; folder in the server and run it. At &lt;code&gt;http://127.0.0.1:8000&lt;/code&gt; you should be able to access your site. Now you can find information about service worker in the &lt;em&gt;Application&lt;/em&gt; tab of DevTools and see some console logs about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taming service worker
&lt;/h2&gt;

&lt;p&gt;That's great! Everything works! So what's the problem? Problem appears, when you want to control caching with service worker by yourself and check it during development without constantly creating production builds.&lt;/p&gt;

&lt;p&gt;I'll show 3 things now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How to run service worker in development server&lt;/li&gt;
&lt;li&gt;How to control cache behavior&lt;/li&gt;
&lt;li&gt;How to use external modules in production build&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  SW in dev server
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Quick warning - SW is disabled in development by default, because it may cache some newly edited scripts/assets and you won't be able to see your changes. Keep that in mind and disable SW in dev if you don't need it to avoid "Why it doesn't change?!" problems.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Another warning - it's probably not the best, most optimal way to do that... but it's simple and works :)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case: we want to have service worker active in development mode and be able to control its caching policy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not diving into details, let's make it happen.&lt;/p&gt;

&lt;p&gt;First of all, you need to install &lt;a href="https://www.npmjs.com/package/serviceworker-webpack-plugin" rel="noopener noreferrer"&gt;serviceworkerW-webpack-plugin&lt;/a&gt; in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; serviceworker-webpack-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in root of your project (next to &lt;code&gt;src&lt;/code&gt; folder) add new file &lt;code&gt;vue.config.js&lt;/code&gt; with that content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vue.config.js&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;path&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;ServiceWorkerWebpackPlugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceworker-webpack-plugin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;configureWebpack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ServiceWorkerWebpackPlugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&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;./src/service-worker.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and modify &lt;code&gt;src/main.js&lt;/code&gt; to include those lines (before &lt;code&gt;createApp&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.js&lt;/span&gt;

&lt;span class="c1"&gt;// other imports...&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;runtime&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;serviceworker-webpack-plugin/lib/runtime&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceWorker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Finally, add &lt;code&gt;service-worker.js&lt;/code&gt; in &lt;code&gt;src&lt;/code&gt; with some "Hello world" content:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/service-worker.js&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;Hello world from our SW!&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;and run the dev server&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;When you navigate to your app in a browser you should see the message from service-worker in the console. Success!&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlling caching - Workbox
&lt;/h3&gt;

&lt;p&gt;Writing SW from scratch may be interesting... but let's make it simple and use &lt;a href="https://developers.google.com/web/tools/workbox" rel="noopener noreferrer"&gt;Workbox&lt;/a&gt; for that. It's already installed, so you just need to import it in the SW script. I'm not going to explain everything from the below snippet, because it's done very clearly on &lt;em&gt;Getting started&lt;/em&gt; page of Workbox. It's just example of setting specific rules for data matching some RegEx (images in that case).&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;// src/service-worker.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workbox-routing&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StaleWhileRevalidate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workbox-strategies&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Plugin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workbox-expiration&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;precacheAndRoute&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workbox-precaching&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;precacheAndRoute&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="nx"&gt;serviceWorkerOption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;registerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(?:&lt;/span&gt;&lt;span class="sr"&gt;png|gif|jpg|jpeg|svg&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StaleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 Days&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Just brief comment about that &lt;code&gt;precacheAndRoute&lt;/code&gt; line - &lt;code&gt;self.serviceWorkerOption.assets&lt;/code&gt; comes from that &lt;code&gt;serviceworker-webpack-plugin&lt;/code&gt; we installed before and contains all the statics assets in our app. &lt;/p&gt;

&lt;p&gt;Now, even if you are in development mode, you should see some workbox logs in the console. On the first page load it will be&lt;br&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%2Fi%2Fegs8exk7sh2kowip9lq0.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%2Fi%2Fegs8exk7sh2kowip9lq0.png" alt="wb1" width="416" height="174"&gt;&lt;/a&gt;&lt;br&gt;
and on subsequent something like that&lt;br&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%2Fi%2Fc49jm3mhxugknlac4912.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%2Fi%2Fc49jm3mhxugknlac4912.png" alt="wb2" width="677" height="229"&gt;&lt;/a&gt;&lt;br&gt;
If you go to &lt;em&gt;Network&lt;/em&gt; tab in DevTools and simulate offline mode, the app should still load properly. &lt;/p&gt;

&lt;p&gt;Great! Two problems solved - we have granular control over our service worker and it works in development mode.&lt;/p&gt;
&lt;h4&gt;
  
  
  Fixing SW on prod
&lt;/h4&gt;

&lt;p&gt;In the meantime, unfortunately we messed up the production version of the app. If you run &lt;code&gt;npm run build&lt;/code&gt; and look at it, it may seem fine at the beginning, but it's not. First of all, on subsequent refreshes you can see&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;New content is available; please refresh.&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;log all the time, although you don't change anything. Also, if you check &lt;em&gt;Application&lt;/em&gt; tab, you will see two service workers all the time - one active, the second one waiting to activate. Even if you force the update, there will be another waiting after the refresh.&lt;/p&gt;

&lt;p&gt;Issue comes from double registration - one SW is registered in &lt;code&gt;main.js&lt;/code&gt;, the second one comes from generated &lt;code&gt;registerServiceWorker.js&lt;/code&gt;. That's the problem I wasn't able to overcome in good way, but I came up with two acceptable solutions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you don't care about that logging coming from &lt;code&gt;registerServiceWorker.js&lt;/code&gt;, just don't import it in &lt;code&gt;src/main.js&lt;/code&gt; and problem will be gone. &lt;/li&gt;
&lt;li&gt;If you want to keep those console logs, but are fine with having SW working &lt;strong&gt;only&lt;/strong&gt; on prod (but keep the control of caching rules) and with a bit more complex way of importing modules in SW, it requires a bit more effort:
Firstly, change &lt;code&gt;vue.config.js&lt;/code&gt; contents to:
&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="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pwa&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;workboxPluginMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;InjectManifest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;workboxOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;swSrc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;src/service-worker.js&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;then revert changes you made in &lt;code&gt;src/main.js&lt;/code&gt; (i.e. remove anything related to &lt;code&gt;serviceworker-webpack-plugin&lt;/code&gt;). Finally, you need to change &lt;code&gt;src/service-worker.js&lt;/code&gt; to &lt;strong&gt;NOT&lt;/strong&gt; use &lt;code&gt;import&lt;/code&gt; and use precaching with different argument. If you want to use some external modules, use &lt;code&gt;importScripts&lt;/code&gt; with CDN link (momentjs below for example; usage is stupid but demonstrates way of doing that). Note how workbox names are expanded now:&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="nf"&gt;importScripts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://momentjs.com/downloads/moment.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;workbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;precaching&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;precacheAndRoute&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="nx"&gt;__precacheManifest&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;cacheExpTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;day&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;cacheTimeLeft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheExpTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;moment&lt;/span&gt;&lt;span class="p"&gt;())).&lt;/span&gt;&lt;span class="nf"&gt;asSeconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;workbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(?:&lt;/span&gt;&lt;span class="sr"&gt;png|ico|gif|jpg|jpeg|svg&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;workbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;strategies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StaleWhileRevalidate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;cacheName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;images&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;workbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Plugin&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="na"&gt;maxEntries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;maxAgeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cacheTimeLeft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 1 day&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Surely there's some 3rd option, which lets you keep logging and everything else, but I don't know, how to configure Webpack properly. If you have any simple solution, I'll be more than happy to read about it in the comments :)&lt;/p&gt;

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

&lt;p&gt;If you want very simple caching technique and keeping handling only static assets by service worker is fine, the generated project is definitely enough. However, if you want more control over your service worker to cache ex. API calls, you need to tweak it somehow. I hope that above tips, how to do that and how to handle the development mode will be useful.&lt;/p&gt;

&lt;p&gt;As said, it's definitely not the best and only solution. It's just a starter option for some Vue newbies (like me) to deal with service workers in a reasonable way. &lt;/p&gt;

</description>
      <category>vue</category>
      <category>pwa</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Windows Terminal conda</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Tue, 26 May 2020 00:27:15 +0000</pubDate>
      <link>https://dev.to/voodu/windows-terminal-conda-d3e</link>
      <guid>https://dev.to/voodu/windows-terminal-conda-d3e</guid>
      <description>&lt;h2&gt;
  
  
  What's the problem?
&lt;/h2&gt;

&lt;p&gt;If you code in Python you've probably encountered the problem of different dependencies required in different projects. Not to search very far, some projects still require Python 2.X to run, while the other work only with Python 3.X.&lt;/p&gt;

&lt;p&gt;Probably on of the first solutions which comes to mind is having some sort of 'environments' for different modules/interpreters. With such an approach you can just switch between different 'environments' when you need it. For example you can have an "Convolutional Neural Networks Environment" with a Tensorflow + OpenCV and "Data Analysis Environment" with Pandas + Matplotlib. Then you can just switch/activate one of them when you need it. Sounds nice, isn't it?&lt;/p&gt;

&lt;h2&gt;
  
  
  conda to the rescue!
&lt;/h2&gt;

&lt;p&gt;Fortunately, you don't have to create some fancy bash/PowerShell scripts by hand to do this. Creators of &lt;a href="https://docs.conda.io/" rel="noopener noreferrer"&gt;conda&lt;/a&gt; have already solved the problem! It enables you to do the exact things described above - create individual, isolated environments with different packages or even whole different Python interpreters. What makes it even better is its popularity and community support. &lt;/p&gt;

&lt;blockquote&gt;
&lt;h3&gt;
  
  
  &lt;a href="https://stackoverflow.com/a/45421527/9375075" rel="noopener noreferrer"&gt;conda vs Miniconda vs Anaconda&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;When you'll start looking for some "conda installation guide" you'll immediately face the problem of those 3... things above. What is it all about? To keep it short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;conda&lt;/strong&gt; is just a package management system. Something like &lt;code&gt;apt-get&lt;/code&gt;, &lt;code&gt;chocolatey&lt;/code&gt; or brand new &lt;a href="https://docs.microsoft.com/en-us/windows/package-manager/winget/" rel="noopener noreferrer"&gt;winget&lt;/a&gt;. It facilitates installation &amp;amp; management of different packages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;miniconda&lt;/strong&gt; is already pre-built and pre-configured collection of the packages. To make it as small as possible, it contains only &lt;code&gt;conda&lt;/code&gt; + its dependencies. If you want to have other packages, you'll need to download them individually with &lt;code&gt;conda&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;anaconda&lt;/strong&gt; is similar to miniconda, but the collection of the pre-installed packages is much bigger. You can still download some packages using &lt;code&gt;conda&lt;/code&gt;, but probably you won't have to if you are just starting and using some common ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's a super-short miniconda quickstart&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download proper &lt;a href="https://docs.conda.io/en/latest/miniconda.html" rel="noopener noreferrer"&gt;installer&lt;/a&gt; and install miniconda (when in doubt, choose the recommended options).&lt;/li&gt;
&lt;li&gt;Run &lt;em&gt;Anaconda Prompt&lt;/em&gt; from Start menu. A terminal window should appear with something like &lt;code&gt;(base) D:\Desktop&amp;gt;&lt;/code&gt;. That &lt;code&gt;(base)&lt;/code&gt; prefix indicates that you're currently using &lt;em&gt;base&lt;/em&gt; (default) conda environment.&lt;/li&gt;
&lt;li&gt;To check, what packages are installed in that environment: &lt;code&gt;conda list&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;To create a new "superNewEnv" environment: &lt;code&gt;conda create --name superNewEnv&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;To install SciPy in this environement: &lt;code&gt;conda install --name superNewEnv scipy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;To activate an environment: &lt;code&gt;conda activate superNewEnv&lt;/code&gt;. You can always see your active environment at the beginning of the prompt (like that &lt;code&gt;base&lt;/code&gt; at the beginning).&lt;/li&gt;
&lt;li&gt;Now, if you run Python inside the &lt;code&gt;superNewEnv&lt;/code&gt; environment you'll be able to use SciPy. What's even better, it can be used &lt;em&gt;only&lt;/em&gt; there, the &lt;code&gt;base&lt;/code&gt; environment is not cluttered.&lt;/li&gt;
&lt;li&gt;Check out the &lt;a href="https://docs.conda.io/projects/conda/en/latest/user-guide/index.html" rel="noopener noreferrer"&gt;docs&lt;/a&gt; or &lt;a href="https://docs.conda.io/projects/conda/en/4.6.0/_downloads/52a95608c49671267e40c689e0bc00ca/conda-cheatsheet.pdf" rel="noopener noreferrer"&gt;cheatsheet&lt;/a&gt; to dive deeper.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  But I want my terminal!
&lt;/h2&gt;

&lt;p&gt;The minor issue with conda is its interoperability with your typical command line or PowerShell. &lt;code&gt;conda&lt;/code&gt; commands won't work there, only in its special prompt. However, it can be partially solved with the new &lt;a href="https://github.com/microsoft/terminal" rel="noopener noreferrer"&gt;Windows Terminal&lt;/a&gt;. I won't describe all its features here, but you should definitely check it out. The thing which is interesting for us right now is its concept of &lt;em&gt;profiles&lt;/em&gt;. In short, they are preconfigured different ways of launching command line, terminal or whatever you call it.&lt;/p&gt;

&lt;p&gt;After installing and launching the terminal, go to its settings by clicking an arrow on the tab bar:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ftf28h9uoe9xvfbjgvfub.GIF" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ftf28h9uoe9xvfbjgvfub.GIF" alt="GIF showing location of the settings"&gt;&lt;/a&gt;&lt;br&gt;
The settings don't have any GUI yet, it's just a JSON file you can modify. Therefore they'll open in some default JSON editor (VS Code in my case).&lt;/p&gt;

&lt;p&gt;In the file look for &lt;code&gt;profiles&lt;/code&gt; property. It has a &lt;code&gt;list&lt;/code&gt; of different profiles created by Windows Terminal automatically. We'll just one more to that list.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's special about miniconda prompt?
&lt;/h3&gt;

&lt;p&gt;If you examine a bit the thing you're launching from the start menu, you'll discover that the shortcut just runs a cmd/PowerShell with some arguments:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;%windir%\System32\cmd.exe "/K" C:\Users\%USERNAME%\miniconda3\Scripts\activate.bat C:\Users\%USERNAME%\miniconda3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;(to find it out yourself, search for Anaconda prompt, click it with right mouse button, "Open File location" and check the properties of the shortcut).&lt;/p&gt;

&lt;p&gt;Now let's have that prompt in our Windows Terminal! It boils down to adding another profile which will launch the cmd/PowerShell as shown above. Just add another element to the &lt;code&gt;list&lt;/code&gt; in &lt;code&gt;profiles&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"guid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{2337f50b-bd5c-4877-b127-d643395b8fe2}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hidden"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Miniconda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commandline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell.exe -ExecutionPolicy ByPass -NoExit -Command &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp; 'C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;%USERNAME%&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;miniconda3&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;shell&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;condabin&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;conda-hook.ps1' ; conda activate 'C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;%USERNAME%&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;miniconda3' &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;


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

&lt;/div&gt;

&lt;p&gt;There are 3 things wort mentioning here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Value of GUID does not matter. Just make sure it's unique among all the profiles&lt;/li&gt;
&lt;li&gt;Value for &lt;code&gt;commandline&lt;/code&gt; differs a bit from the one above, as I decided to use PowerShell instead of cmd&lt;/li&gt;
&lt;li&gt;Watch out for backslashes and escape all of them + quotemarks properly!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final touches
&lt;/h2&gt;

&lt;p&gt;The new profile "Miniconda" should be available in the terminal dropdown. Just to make it a bit prettier&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Let's add the conda icon! &lt;/p&gt;

&lt;p&gt;First find some version of it you like (ex. &lt;a href="https://www.psych.mcgill.ca/labs/mogillab/anaconda2/lib/python2.7/site-packages/anaconda_navigator/static/images/anaconda-icon-1024x1024.png" rel="noopener noreferrer"&gt;that one&lt;/a&gt;). Save it somewhere on your machine and add &lt;code&gt;icon&lt;/code&gt; field to the Miniconda profile:&lt;br&gt;
&lt;code&gt;"icon": "path\\to\\your\\icon.png"&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Always display "Miniconda" in the tab name. &lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;"suppressApplicationTitle": true&lt;/code&gt; to the profile.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Final result:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F41zfomd35alznp9mj5t0.GIF" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F41zfomd35alznp9mj5t0.GIF" alt="GIF with final result"&gt;&lt;/a&gt;&lt;br&gt;
Final profile settings:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"guid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{2337f50b-bd5c-4877-b127-d643395b8fe2}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hidden"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Miniconda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"suppressApplicationTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commandline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"powershell.exe -ExecutionPolicy ByPass -NoExit -Command &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp; 'C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;%USERNAME%&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;miniconda3&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;shell&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;condabin&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;conda-hook.ps1' ; conda activate 'C:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Users&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;%USERNAME%&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;miniconda3' &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"icon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"%OneDrive%&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Pictures&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;Saved Pictures&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;conda.png"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;PS.&lt;br&gt;
To make your terminal look prettier check out that &lt;a href="https://www.hanselman.com/blog/HowToMakeAPrettyPromptInWindowsTerminalWithPowerlineNerdFontsCascadiaCodeWSLAndOhmyposh.aspx" rel="noopener noreferrer"&gt;blog post by Scott Hanselman&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>windowsterminal</category>
      <category>conda</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Starting with DevOps on minigame project</title>
      <dc:creator>Piotr Ładoński</dc:creator>
      <pubDate>Wed, 20 May 2020 12:42:52 +0000</pubDate>
      <link>https://dev.to/voodu/minigame-dotfall-20k3</link>
      <guid>https://dev.to/voodu/minigame-dotfall-20k3</guid>
      <description>&lt;h2&gt;
  
  
  DotFall
&lt;/h2&gt;

&lt;p&gt;DotFall is a super simple game inspired by an older mobile game, Rapid Roll (remake can be found &lt;a href="https://play.google.com/store/apps/details?id=com.eskimob.rr"&gt;here&lt;/a&gt;). You just fall down and try to reach the highest score you can! Watch out, because the longer you play, the faster you fall!&lt;/p&gt;

&lt;p&gt;I did it as a part of Faculty Open Day at my University. The game was a small competition. Its participants were supposed to get the highest score, take a snapshot of it and win some small prizes.&lt;/p&gt;

&lt;p&gt;Should work well on desktop browser as well as mobile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Link
&lt;/h2&gt;

&lt;p&gt;Go to the &lt;a href="https://dotfall.azurewebsites.net/"&gt;website&lt;/a&gt; and play by clicking/tapping left or right side of the screen to move the ball.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link to Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--566lAguM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Voodu"&gt;
        Voodu
      &lt;/a&gt; / &lt;a href="https://github.com/Voodu/dot-fall"&gt;
        dot-fall
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Simple 2D game in Typescript using Phaser.io
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;&lt;a href="https://dev.azure.com/PiotrLadonski/DotFall/_build/latest?definitionId=2?branchName=master" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/04d19b5d4eef392c35d1bdc15030624ec420b1cad11657a859772ea19825de78/68747470733a2f2f6465762e617a7572652e636f6d2f50696f74724c61646f6e736b692f446f7446616c6c2f5f617069732f6275696c642f7374617475732f646f7466616c6c2532302d253230312532302d25323043493f6272616e63684e616d653d6d6173746572" alt="Build Status"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
DotFall&lt;/h1&gt;
&lt;p&gt;Simple game written using Phaser 3 and Typescript
Main aim is to fall with the ball as long as you can.&lt;/p&gt;
&lt;p&gt;To build the game, run &lt;code&gt;npm install&lt;/code&gt; in project root and then&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;gulp&lt;/code&gt; for one-time production build with minifying and obfuscating&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gulp watch&lt;/code&gt; for continuous watching for changes (useful for development)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To launch the game, open created &lt;code&gt;dist/index.html&lt;/code&gt;
&lt;br&gt;Build process is poorly configured, so after adding new file to &lt;code&gt;src&lt;/code&gt; your need to restart gulp.&lt;/p&gt;
&lt;h3&gt;
Gulp tasks&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;bundle&lt;/code&gt; - creates uglified and obfuscated &lt;code&gt;bundle.js&lt;/code&gt; file in &lt;code&gt;dist&lt;/code&gt; directory from all &lt;code&gt;.ts&lt;/code&gt; files in &lt;code&gt;src&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;copy-html&lt;/code&gt; - copies all &lt;code&gt;.html&lt;/code&gt; files from &lt;code&gt;src&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;copy-css&lt;/code&gt; - copies all &lt;code&gt;.css&lt;/code&gt; files from &lt;code&gt;src&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;copy-assets&lt;/code&gt; - copies &lt;code&gt;assets&lt;/code&gt; folder from &lt;code&gt;src&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;copy-other&lt;/code&gt; - copies &lt;code&gt;.png&lt;/code&gt;,&lt;code&gt;.xml&lt;/code&gt;,&lt;code&gt;.ico&lt;/code&gt;,&lt;code&gt;.svg&lt;/code&gt;,&lt;code&gt;.webmanifest&lt;/code&gt; files from &lt;code&gt;src&lt;/code&gt; to &lt;code&gt;dist&lt;/code&gt; (favicon files)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;copy-all&lt;/code&gt; - runs &lt;code&gt;copy-html&lt;/code&gt;…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Voodu/dot-fall"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;The idea for DotFall is super simple so I decided to learn some basic DevOps&lt;sup id="fnref1"&gt;1&lt;/sup&gt;, CD and other auto-magic stuff with that. I also wanted to make it bug-free and really playable by the users (not some I-will-fix-that-later project).&lt;/p&gt;

&lt;p&gt;The game itself is written in &lt;a href="https://phaser.io/"&gt;Phaser framework&lt;/a&gt;. Natively it's written in pure JavaScript and personally I prefer using TypeScript. So that's my small success #1 - with help from our &lt;a href="https://www.google.com/"&gt;uncle&lt;/a&gt; and &lt;a href="https://stackoverflow.com/"&gt;cousin&lt;/a&gt; I made Phaser work with TypeScript.&lt;/p&gt;

&lt;p&gt;The second thing I was quite satisfied with was configuration of &lt;a href="https://gulpjs.com/"&gt;gulp&lt;/a&gt; - I've never used it before, but after some messing around with it I managed to successfully use it in the project to automate all the building, bundling etc.&lt;/p&gt;

&lt;p&gt;After everything was coded and worked more or less okay, it was time for publishing. I wanted to make it as smooth as possible. First of all I'd set up an App Service on Microsoft Azure to host the content of my game. The best part of it was the ease of usage - I could just publish the changes directly from my VS Code and it worked!&lt;/p&gt;

&lt;p&gt;The last step was to have some sort of CD. As I'd used Azure for hosting, I've decided to use Azure Pipelines for easier integrations. After some struggle, playing with YAML files and other weird configuration it finally worked! After pushing changes to the &lt;code&gt;master&lt;/code&gt; branch, code was processed by the pipelines (gulp bundling included), then automagically detected by Azure and reflected on the live page. Full success! 😁&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bQWnD81A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/AqU2HEY.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bQWnD81A--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://i.imgur.com/AqU2HEY.jpg" alt="Success kid" width="750" height="422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To complete the tech stack topic - I'm not that console-freak type, so I've used GitKraken to help me with "gitting around" 🙂 &lt;/p&gt;

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

&lt;p&gt;I have a small challenge for you - try to break the game and set the high score by hand! The game has no backend (I promise!) so the score is somehow stored on your machine. I tried the manual modification to be as hard as possible (to prevent cheating in the competition), but, obviously, it can't be prevented completely by client side-only application.&lt;/p&gt;

&lt;p&gt;Currently, if you want to build something similar (static website with CD from GitHub), I'd definitely recommend checking out 🆕 &lt;a href="https://azure.microsoft.com/services/app-service/static/"&gt;Azure Static Website&lt;/a&gt; 🆕. It does most of the things I did above out of the box.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;DevOps only. The code itself is a mess 😅 ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>octograd2020</category>
      <category>githubsdp</category>
    </item>
  </channel>
</rss>
