goulet.dev
Writing down what I'm learning as a software developer - TypeScript, PWAs, web components, Jamstack, etc
2024-03-28T18:00:33Z
https://goulet.dev/
Wes Goulet
wes@goulet.dev
https://goulet.dev/static/logo-180.png
https://goulet.dev/static/logo.svg
268bd2
Avoiding Git Problems When Installing a Theme to Hugo
2018-08-25T00:00:00Z
https://goulet.dev/posts/avoiding-git-problems-when-installing-a-theme-to-hugo/
<h2>tl;dr</h2>
<p>If you didn't install your hugo theme as a submodule then don't forget to delete the <code>.git</code> folder from the theme's folder.</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># assuming you start from repo root</span>
<span class="token builtin class-name">cd</span> themes
<span class="token comment"># install the "hyde" theme</span>
<span class="token function">git</span> clone https://github.com/spf13/hyde.git
<span class="token function">rm</span> <span class="token parameter variable">-rf</span> hyde/.git
<span class="token comment"># now go back to repo root and stage the new theme folder</span>
<span class="token builtin class-name">cd</span> <span class="token punctuation">..</span>
<span class="token function">git</span> <span class="token function">add</span> themes/hyde</code></pre>
<p>If you forget to delete the theme's <code>.git</code> folder then you might see the error <code>fatal: in unpopulated submodule</code>. To fix this, remove the theme folder from the git cache:</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># assuming you start from repo root</span>
<span class="token comment"># remove the theme folder from the git cache (but don't remove from file system)</span>
<span class="token function">git</span> <span class="token function">rm</span> <span class="token parameter variable">--cached</span> themes/hyde
<span class="token comment"># now you can add the theme folder</span>
<span class="token function">git</span> <span class="token function">add</span> themes/hyde</code></pre>
<h2>long version</h2>
<p>If you are new to hugo you might run into some git troubles after installing a theme.</p>
<p>Hugo uses git to install themes and if you are like me you might not have read (or glossed over) this little nugget from the <a href="https://gohugo.io/themes/installing-and-using-themes/#install-all-themes">theme documentation</a>:</p>
<blockquote>
<p>Before you use a theme, remove the .git folder in that themeβs root folder. Otherwise, this will cause problem if you deploy using Git.</p>
</blockquote>
<p>If you install a theme via <code>git clone</code> but forget to delete the <code>.git</code> folder in that theme's folder then your newly-installed theme will not get picked up when you attempt to commit the changes to your site.</p>
<p>Here are the commands I use to install themes (same as tl;dr above):</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># assuming you start from repo root</span>
<span class="token builtin class-name">cd</span> themes
<span class="token comment"># using hyde theme for this example</span>
<span class="token function">git</span> clone https://github.com/spf13/hyde.git
<span class="token function">rm</span> <span class="token parameter variable">-rf</span> hyde/.git
<span class="token comment"># now go back to repo root and stage the new theme folder</span>
<span class="token builtin class-name">cd</span> <span class="token punctuation">..</span>
<span class="token function">git</span> <span class="token function">add</span> themes/hyde</code></pre>
<h4>What if I forget to remove the <code>.git</code> folder?</h4>
<p>If you already committed before deleting the theme's <code>.git</code> folder then you might see the error <code>fatal: in unpopulated submodule</code> whenever you try to <code>git add</code> that theme's folder.</p>
<p>To fix this remove the .git folder from your theme's folder as described above and then remove the theme folder from the git cache (same as tl;dr above):</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># assuming you start from repo root</span>
<span class="token comment"># remove the theme folder from the git cache (but don't remove from file system)</span>
<span class="token function">git</span> <span class="token function">rm</span> <span class="token parameter variable">--cached</span> themes/hyde
<span class="token comment"># now you can add the theme folder</span>
<span class="token function">git</span> <span class="token function">add</span> themes/hyde</code></pre>
<h4>Or use submodules</h4>
<p>Of course, you can always install themes as submodules and not have to deal with deleting any <code>.git</code> folder... but then you have to deal with submodules. Personally, I find submodules annoying (I always forget to sync them after cloning a repo), so I'd rather just use <code>git clone</code> and delete the <code>.git</code> folder instead of using <code>git submodule add</code>. For the sake of completeness this is how to install the theme as a submodule:</p>
<pre class="language-bash"><code class="language-bash"><span class="token comment"># assuming you start from repo root</span>
<span class="token builtin class-name">cd</span> themes
<span class="token function">git</span> submodule <span class="token function">add</span> https://github.com/spf13/hyde.git</code></pre>
Fixing Node Library Not Loaded icu4c Error
2018-09-01T00:00:00Z
https://goulet.dev/posts/node-dyld-library-not-loaded-icu4c/
<p>Recently I sat down at my mac to start working on one of my React apps and when I tried to run <code>yarn start</code> I got the following error:</p>
<pre class="language-bash"><code class="language-bash">dyld: Library not loaded: /usr/local/opt/icu4c/lib/libicui18n.61.dylib
Referenced from: /usr/local/opt/node@8/bin/node
Reason: image not found
Abort trap: <span class="token number">6</span></code></pre>
<p>The error message mentioned node so I tried to run <code>node -v</code> and got the same error so I knew I'd narrowed the problem down to node and some dependency named icu4c. As with most computer-related issues, I figured a re-install might be the quickest path to fixing things, and sure enough it did:</p>
<pre class="language-bash"><code class="language-bash">brew uninstall --ignore-dependencies <span class="token function">node</span>
brew <span class="token function">install</span> <span class="token function">node</span></code></pre>
<p>Notice I included the <code>--ignore-dependencies</code> flag on the uninstall because yarn depends on node and brew won't let me uninstall without passing that flag. It's not a problem though since I'm installing node again on the next line.</p>
<p>And now I can <code>yarn start</code> my react project and get back to development.</p>
Handling Keyboard Shortcuts in React
2018-09-08T00:00:00Z
https://goulet.dev/posts/react-handle-keyboard-shortcuts/
<p>This morning I was working on adding a settings page to <a href="https://goulet.dev/portfolio/clean-calendar/">Clean Calendar</a>. Right now there aren't many settings to toggle so I didn't feel a full page navigation was warranted so I decided to use a quick, slide-up dialog.</p>
<p>As a user, whenever I come across dialogs or modals or any other, uhh, "dismissable" UX patterns I instinctively hit the Escape key on my keyboard to try and dismiss them. If the dialog dismisses when I hit Escape then I think, "This app is solid and cares about UX". Maybe I'm the only one who thinks such things π. Regardless, I could not continue on with building out the settings dialog without being able to dismiss it via the keyboard.</p>
<h2>Use a library or write it myself</h2>
<p>So I did some quick research and there seems to be a <a href="https://github.com/greena13/react-hotkeys">few</a> <a href="https://github.com/avocode/react-shortcuts">different</a> libraries for dealing with keyboard shortcuts in React. I'm all for pulling in libraries when needed, but at this point I'm just trying to capture one little key press on one little screen... I'm not ready to pull in a library. Plus I want to learn how to do this "by hand" so that I can learn something new today.</p>
<h2>How to handle the Escape key press</h2>
<p>Javascript has a simple onKeyDown event, so I just used that. The first step was figuring out what element to listen to. I had 2 options, I could have listened on the <code>Dialog</code> component or on the <code>Settings</code> component. Since the whole point of listening to the escape key is to dismiss a dialog (and not a settings page) I listened to the onKeyDown event of the <code>Dialog</code> component.</p>
<p>So my first step was to add the onKeyDown event to the Dialog:</p>
<pre class="language-js"><code class="language-js"><span class="token operator"><</span>Dialog open<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>state<span class="token punctuation">.</span>showSettingsDialog<span class="token punctuation">}</span> onKeyDown<span class="token operator">=</span><span class="token punctuation">{</span><span class="token keyword">this</span><span class="token punctuation">.</span>handleKeyDown<span class="token punctuation">}</span><span class="token operator">></span>
<span class="token operator"><</span>Settings <span class="token operator">/</span><span class="token operator">></span>
<span class="token operator"><</span><span class="token operator">/</span>Dialog<span class="token operator">></span></code></pre>
<p>And my next step was to add an arrow function defining <code>handleKeyDown</code>:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">private</span> <span class="token function-variable function">handleKeyDown</span> <span class="token operator">=</span> <span class="token parameter">event</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>event<span class="token punctuation">.</span>keyCode <span class="token operator">===</span> <span class="token number">27</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">this</span><span class="token punctuation">.</span><span class="token function">setState</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token operator">...</span><span class="token keyword">this</span><span class="token punctuation">.</span>state<span class="token punctuation">,</span> <span class="token literal-property property">showSettingsDialog</span><span class="token operator">:</span> <span class="token boolean">false</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>And now whenever the dialog is shown and I can hit the escape key and the dialog will be dismissed.</p>
<p><img src="https://goulet.dev/posts/react-handle-keyboard-shortcuts/esc-dismiss-dialog.gif" alt="Escape dismissing dialog" /></p>
Fixing CSS Tap Effects on iOS
2018-11-18T00:00:00Z
https://goulet.dev/posts/fix-css-tap-effects-on-ios/
<p>While working on a <a href="https://github.com/wes-goulet/wc-menu-button">simple menu icon</a> I came across an issue that was bugging me on iOS. My component has a simple <code>:hover</code> effect where it changes colors, which is a great little UX indicator that the component is interactive when your mouse hovers over it. But when I'm on iOS there are a couple of problems when I tap my component:</p>
<ol>
<li>There is a subtle gray highlight effect when I tap on the component</li>
<li>The component stays the <code>:hover</code> color after I tap on it until I tap somewhere else</li>
</ol>
<p>Both issues can be seen here:</p>
<p><img src="https://goulet.dev/posts/fix-css-tap-effects-on-ios/tap-effects-css-ios-before.gif" alt="Bad Tap Effect on iOS" /></p>
<p>The default tap effects just makes my web component feel very much "web" and not so much native-feeling.</p>
<p>Thankfully both issues can be fixed, and best of all, both issues can be fixed in CSS - no Javascript hacks required!</p>
<p>For the first issue, my friend pointed out that he encountered the same issue when working on <a href="https://www.instachef.app/">InstaChef</a>. He told me there is a special webkit CSS property that I can set.</p>
<pre class="language-css"><code class="language-css"><span class="token selector">.menu-icon</span> <span class="token punctuation">{</span>
<span class="token property">-webkit-tap-highlight-color</span><span class="token punctuation">:</span> transparent<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>For the second issue, you can use the <code>hover</code> media query. As of November 2018 <a href="https://caniuse.com/#feat=css-media-interaction">not all browsers</a> support the hover media query, but at least Safari does and that's what I care about, so I simply set any hover CSS in a media query like so:</p>
<pre class="language-css"><code class="language-css"><span class="token atrule"><span class="token rule">@media</span> <span class="token punctuation">(</span><span class="token property">hover</span><span class="token punctuation">:</span> hover<span class="token punctuation">)</span></span> <span class="token punctuation">{</span>
<span class="token selector">.menu-icon:hover</span> <span class="token punctuation">{</span>
<span class="token property">opacity</span><span class="token punctuation">:</span> <span class="token function">var</span><span class="token punctuation">(</span>--wc-menu-button-hover-opacity<span class="token punctuation">,</span> 0.75<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p><img src="https://goulet.dev/posts/fix-css-tap-effects-on-ios/tap-effects-css-ios-after.gif" alt="Good Tap Effect on iOS" /></p>
<p>And the finished result looks a lot more like a native button interaction, and a lot less like a 2000's web button interaction.</p>
Consuming a Web Component in React in Typescript
2018-12-01T00:00:00Z
https://goulet.dev/posts/consuming-web-component-react-typescript/
<p><img src="https://goulet.dev/posts/consuming-web-component-react-typescript/header.png" alt="React and Web Components" /></p>
<p>After building <a href="https://github.com/wes-goulet/wc-menu-button">my first web component</a> I wanted to use it in my React app that was written in Typescript. I found some blog posts and tutorials about using web components in React apps, but none of those posts/tutorials were Typescript-based, so when I went to add my custom element I got the error <code>[ts] Property 'wc-menu-button' does not exist on type 'JSX.IntrinsicElements'. [2339]</code>.</p>
<p><img src="https://goulet.dev/posts/consuming-web-component-react-typescript/ts-error.png" alt="TypeScript Error JSX.IntrinsicElements" /></p>
<p>The problem is React only knows about standard HTML Elements, so when I go putting my custom element in there, it doesn't know about it and shows me the error. Obviously Javascript React projects don't have this error because there are no types (just mayhem and run-time bugs π).</p>
<p>There are 2 ways to fix this. One way is in the web component, the other way is in the React app.</p>
<h2>Option 1: Type Definitions in the web component</h2>
<p>If you are the author of the web component you can declare your component as part of JSX's IntrinsicElements, like this:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">declare</span> global <span class="token punctuation">{</span>
<span class="token keyword">namespace</span> <span class="token constant">JSX</span> <span class="token punctuation">{</span>
<span class="token keyword">interface</span> <span class="token class-name">IntrinsicElements</span> <span class="token punctuation">{</span>
<span class="token string-property property">"my-element"</span><span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>You might think that setting the type of <code>my-element</code> to any is less than ideal, and you'd be right. Wouldn't it be better to define your attributes in an interface then set your custom element to that type? Something like this:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">declare</span> global <span class="token punctuation">{</span>
<span class="token keyword">namespace</span> <span class="token constant">JSX</span> <span class="token punctuation">{</span>
<span class="token keyword">interface</span> <span class="token class-name">IntrinsicElements</span> <span class="token punctuation">{</span>
<span class="token string-property property">"my-element"</span><span class="token operator">:</span> MyElementAttributes<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">interface</span> <span class="token class-name">MyElementAttributes</span> <span class="token punctuation">{</span>
name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>This looks well and good, until you try to actually use that custom element in a React project and need to set the <code>key</code> or <code>ref</code> attributes. You'll be met with an error like <code>[ts] Property 'ref' does not exist on type 'MyElementAttributes'. [2339]</code>.</p>
<p><img src="https://goulet.dev/posts/consuming-web-component-react-typescript/ref-error.png" alt="ref property does not exist" /></p>
<p>The <code>MyElementAttributes</code> needs to extend the React <code>HTMLAttributes</code> class, like this:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">interface</span> <span class="token class-name">MyElementAttributes</span> <span class="token keyword">extends</span> <span class="token class-name">HTMLAttributes</span> <span class="token punctuation">{</span>
name<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>But if you try and compile your web component now then tsc (the Typescript compiler) will give you an error because it doesn't know what the <code>HTMLAttributes</code> type is. You'll need to take a dev dependency on React types (<code>@types/react</code>). But now your simple web component projects takes a dependency on React types, which might feel a little backward - your standards-based custom element doesn't need to know about some front-end framework, right? Or maybe it's fine with you. Either take that dev dependency on react, or you can just say your <code>custom-element</code> is of type <code>any</code> and make sure you have good documentation around the attributes (you should have good documentation anyway, actually).</p>
<h3>Or use a tool to help</h3>
<p>If you don't want to deal with generating the <code>IntrinsicElements</code> types yourself, you can use a tool like <a href="https://stenciljs.com/">StencilJS</a> to build your web component. It will take care of types, including a global declare to define your web component inside the <code>IntrinsicElements</code> interface.</p>
<p><em>Caveat for Stencil - there might currently be <a href="https://github.com/ionic-team/stencil/issues/1090">a bug</a> with Stencil type generation and React, so until that is fixed you can also put some extra info in your docs about how to handle type declarations in the React app</em></p>
<h3>Or...</h3>
<p>Make some good documentation and tell consumers of your web component to declare their own types, which leads us to...</p>
<h2>Option 2: Type Definition in the React app</h2>
<p>Similar concept to option 1, so I'll keep this brief. With this options you make the type declaration file in the React app instead of in the web component. I usually just make a <code>declarations.d.ts</code> file and put the type declarations there:</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">declare</span> <span class="token keyword">namespace</span> <span class="token constant">JSX</span> <span class="token punctuation">{</span>
<span class="token keyword">interface</span> <span class="token class-name">IntrinsicElements</span> <span class="token punctuation">{</span>
<span class="token string-property property">"wc-menu-button"</span><span class="token operator">:</span> <span class="token builtin">any</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Now my React app compiles and consumes my web component.</p>
<p>If you'd like to see a sample in action, I made a sample repo of a React app consuming a web component using option 2. Besides declaring types, this repo also demonstrates how to install, how to set attributes, and how to use <code>ref</code> to set properties of the web component.</p>
<p><a href="https://github.com/wes-goulet/sample-react-ts-consumes-web-component">React app in Typescript consuming wc-menu-button sample repo</a></p>
<p><em>I also made a repo in plain old Javascipt <a href="https://github.com/wes-goulet/sample-react-consumes-web-component">here</a>.</em></p>
<!-- ### Attributes vs Properties
Element attributes can only accept strings, not objects
React has open issue around handling custom element - https://github.com/facebook/react/issues/11347
Some people use helper to bind props - https://github.com/ionic-team/ionic-react-conference-app/blob/master/src/utils/stencil.ts
### Attribute names in HTML are different than property name
### Script Tag vs Stencil's defineCustomElements -->
Glob Pattern Not Working? Don't Forget the 'Quotes'
2019-03-30T00:00:00Z
https://goulet.dev/posts/quotes-around-globs/
<p>Our team uses <a href="https://prettier.io/">prettier</a> to keep the formatting of our javascript/typescript code consistent. Recently, I was adding a couple of NPM scripts for checking and fixing prettier formatting to one of our repos.</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"pretty"</span><span class="token operator">:</span> <span class="token string">"prettier -l **/*.{js,ts}"</span><span class="token punctuation">,</span>
<span class="token property">"pretty:fix"</span><span class="token operator">:</span> <span class="token string">"prettier --write **/*.{js,ts}"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>That glob pattern says "in any directory, look for any file ending in <code>.js</code> or <code>.ts</code>".</p>
<p>So I fixed up all formatting by running <code>npm run pretty:fix</code> and then I committed that change. I also added a CI job that runs <code>npm run pretty</code> just to make sure we catch formatting issues in PRs automatically. Then I made my PR and much to my surprise, the new CI job failed.</p>
<p>Turns out when I was running locally my glob pattern wasn't checking every file, but in the CI environment (which runs a different shell than my dev box) it was finding all files. A <a href="https://twitter.com/threeve">much-smarter-than-me coworker</a> pointed out that I didn't put quotes around my glob pattern, which means the shell determines how to glob instead of passing the glob string to prettier. So I updated the package.json with:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"pretty"</span><span class="token operator">:</span> <span class="token string">"prettier -l '**/*.{js,ts}'"</span><span class="token punctuation">,</span>
<span class="token property">"pretty:fix"</span><span class="token operator">:</span> <span class="token string">"prettier --write '**/*.{js,ts}'"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre>
<p>Notice the single quote marks around the glob patterns. Note, I could have also used escaped double quotes: <code>\"**/*.{js,ts}\"</code>.</p>
<p><strong>And <a href="https://youtu.be/IJw2lFWJ4aY">that's why you always</a> put quotes around glob patterns.</strong></p>
iOS 13 PWA Improvements
2019-08-11T00:00:00Z
https://goulet.dev/posts/ios13-pwa-improvements/
<p><img src="https://goulet.dev/posts/ios13-pwa-improvements/ios13.png" alt="iOS 13 + PWA" /></p>
<blockquote>
<p>Read about <a href="https://goulet.dev/posts/ios13-pwa-improvements/%22/posts/ios14-pwa-improvements%22">iOS 14 PWA Improvements here</a>.</p>
</blockquote>
<p>I just installed the iOS 13 public beta on my iPhone today and wanted to see what improvements are in store for Progressive Web Apps (PWAs) running on iOS. There's still a lot to dig into, but I wanted to capture my initial findings here. I'll try and keep this post up to date as I discover new improvements in subsequent beta releases.</p>
<h2>Improvements</h2>
<p>There are a few subtle, welcome improvements that make PWA's feel more like native apps.</p>
<h3>Force Quit</h3>
<p>When iOS 12 was released it brought the welcome feature of remembering a PWAs state after you left the app. While this feature was helpful, it was not implemented the same way for PWAs as for native apps, which led to a different user experience around the app's lifecycle. The most annoying issue (in my opinion) was if you force quit a PWA and then relaunch you will see the previous state... there is no way to get a fresh app start (short of restarting your phone). This is because the OS would not actually keep the PWA in memory, but rather save its state to disk and then restore it whenever the app came back to the foreground (whether the app was in the back stack or not). You can see an example below, notice how the side drawer is open, and stays open even after I force quit and relaunch.</p>
<lite-youtube videoid="jSnkT5htV6A">
<a class="lite-youtube-fallback" href="https://www.youtube.com/watch?v=jSnkT5htV6A">Watch on YouTube: "iOS12 Force Quit PWA"</a>
</lite-youtube>
<p>Now, on iOS 13 you can force quit and when you relaunch the app you will be in a fresh state π</p>
<lite-youtube videoid="6MeGccGsF7I">
<a class="lite-youtube-fallback" href="https://www.youtube.com/watch?v=6MeGccGsF7I">Watch on YouTube: "iOS 13 Force Quit PWA"</a>
</lite-youtube>
<h3>Faster Rehydration</h3>
<p>Related to the previous point, it seems when you switch apps to a PWA it rehydrates faster. I'm not totally sure yet if the OS actually keeps the app in memory or if the restoring of state from disk is just faster. I'll see if I can find out more about this one... it might just be a classic case of <a href="https://www.windowscentral.com/seems-faster">seems faster</a> π</p>
<h3>Favicon Logo is Used</h3>
<p>This isn't really specific to PWAs, but a welcome feature I noticed. Safari's Share Sheet now shows the icon for a web page. It seems this is pulled from <code>rel=logo</code> in the page's HTML.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>icon<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>image/png<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>logo.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
<p><img src="https://goulet.dev/posts/ios13-pwa-improvements/safari-icon.png" alt="Safari shows page icon" /></p>
<h2>What Has Not Been Improved</h2>
<p>Unfortunately, there are some web/PWA features that still don't work on iOS13. This isn't an exhaustive list, just a couple of things I've noticed so far.</p>
<h3>Background Audio</h3>
<p><s>The first thing I checked when I installed iOS 13 was background audio because <a href="https://backgroundnoise.app/">some PWAs</a> really need it. I should have known background audio would still be busted, given the lack of activity on <a href="https://bugs.webkit.org/show_bug.cgi?id=198277">the webkit bug</a>. Maybe Apple will fix it by iOS 13 RTM! <em>Holds breathe... passes out</em></s></p>
<blockquote>
<p><s>Do me a favor and go comment on <a href="https://bugs.webkit.org/show_bug.cgi?id=198277">the webkit bug</a> or <a href="https://twitter.com/wes_goulet/status/1133246521753759744">retweet this</a> to help get this on the Webkit team's radar.</s></p>
</blockquote>
<p>It's fixed as of iOS 15.4! See <a href="https://twitter.com/wes_goulet/status/1491238784020905986?s=20">this tweet</a>.</p>
<h3>Manifest.json Icon Ignored</h3>
<p>The standard way to define icons for a PWA is to set the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/icons">icons object in manifest.json</a>. Unfortunately iOS ignores that so if you want your PWA icon to show up properly on the user's home screen you will still need to add the following to your HTML:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>apple-touch-icon<span class="token punctuation">"</span></span> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>192x192<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./logo.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
<p>That's easy enough, but it would be nice if you only had to declare your icons in one place and it just works for Android <em>and</em> iOS (and Windows and Linux, etc, etc).</p>
How to Build a PWA
2019-08-31T00:00:00Z
https://goulet.dev/posts/how-to-make-a-pwa/
<p><img src="https://goulet.dev/posts/how-to-make-a-pwa/pwa.png" alt="PWA" /></p>
<h2>What is a Progressive Web App (PWA)?</h2>
<p>I'm trying to keep this post simple so I'll define what a PWA is in my own simple terms. <b>A PWA is a web app that uses a set of browser/OS enhancements to make it feel more like a native app.</b></p>
<blockquote>
<p>If you want a more official definition see <a href="https://en.wikipedia.org/wiki/Progressive_web_app">wikipedia</a> or <a href="https://developers.google.com/web/progressive-web-apps/">Google's Web Developer site</a>.</p>
</blockquote>
<h2>PWA Basics</h2>
<p>How do you enhance a website/web app into a Progressive Web App? <a href="https://developers.google.com/web/progressive-web-apps/checklist">Here's a nice PWA checklist</a>... but that's quite a list. Let's just start with the basics and keep it simple, eh?</p>
<p>To me a PWA has at least the following 4 characteristics/features:</p>
<h3>1. Secure (served over HTTPS)</h3>
<p>I used to think serving content over HTTPS was a difficult, expensive undertaking... and then <a href="https://letsencrypt.org/">Let's Encrypt</a> came along and made it free... and then <a href="https://netlify.com/">Netlify</a> came along and made it crazy-simple (and still free). In fact, hosting static sites on a CDN with HTTPS custom domains is as simple as pushing a git commit thanks to Netlify. This isn't an ad, by the way. I just like simple tools that work well.</p>
<p>So sign up for Netlify and hook up your github/gitlab/bitbucket repo and you'll be serving your site/app over HTTPS in no time. For more details, <a href="https://www.netlify.com/blog/2016/09/29/a-step-by-step-guide-deploying-on-netlify/">here is netlify's own guide</a> on deployment.</p>
<p>If you already have hosting setup then there is probably some tutorial a quick internet search away about setting up SSL/HTTPS. Just make sure you do your users a favor and redirect all HTTP traffic to HTTPS (Netlify does this for you, btw) so users don't ever see this:</p>
<p><img src="https://goulet.dev/posts/how-to-make-a-pwa/insecure.png" alt="Not Secure Connection" /></p>
<h3>2. Fullscreen / Standalone</h3>
<p>I don't think every web app should be pinned to the home screen and act like a standalone app - some web apps are rarely used and work just fine as a favorite/bookmark in the browser. But if a user wants to get to your web app faster and pin it to their home screen/desktop, then let's make it a good experience by not showing the browser "shell".</p>
<figure>
<img src="https://goulet.dev/posts/how-to-make-a-pwa/browser-vs-fullscreen.png" alt="Screenshots of Background Noise, one in Safari and one in standalone" />
<figcaption>In the browser on the left, standalone on the right.</figcaption>
</figure>
<p>To make your web app show up "outside" the browser (like the image on the right), you just need to add a <a href="https://developer.mozilla.org/en-US/docs/Web/Manifest">web app manifest</a> to your page and set <code>display</code> to <code>standalone</code>. An example manifest looks like:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"My App"</span><span class="token punctuation">,</span>
<span class="token property">"scope"</span><span class="token operator">:</span> <span class="token string">"/"</span><span class="token punctuation">,</span>
<span class="token property">"display"</span><span class="token operator">:</span> <span class="token string">"standalone"</span><span class="token punctuation">,</span>
<span class="token property">"start_url"</span><span class="token operator">:</span> <span class="token string">"./index.html"</span><span class="token punctuation">,</span>
<span class="token property">"theme_color"</span><span class="token operator">:</span> <span class="token string">"#b859ed"</span><span class="token punctuation">,</span>
<span class="token property">"background_color"</span><span class="token operator">:</span> <span class="token string">"#000000"</span><span class="token punctuation">,</span>
<span class="token property">"icons"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token punctuation">{</span>
<span class="token property">"src"</span><span class="token operator">:</span> <span class="token string">"./icons/icon-72x72.png"</span><span class="token punctuation">,</span>
<span class="token property">"sizes"</span><span class="token operator">:</span> <span class="token string">"72x72"</span><span class="token punctuation">,</span>
<span class="token property">"type"</span><span class="token operator">:</span> <span class="token string">"image/png"</span>
<span class="token punctuation">}</span>
<span class="token punctuation">]</span>
<span class="token punctuation">}</span></code></pre>
<p>And then in your HTML make sure you link to your manifest:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- other stuff in head --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>manifest<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./manifest.json<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>body</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- body stuff --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>body</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<p>Both Android and iOS respect the display property in your manifest so that keeps it simple. Unfortunately, iOS doesn't respect everything in the manifest, which brings us too...</p>
<h2>3. Launch Icon</h2>
<p>A good PWA experience involves a real icon for your app when pinned to home screen.</p>
<figure>
<img src="https://goulet.dev/posts/how-to-make-a-pwa/no-icon-vs-icon.jpg" alt="Screenshot of iOS homescreen" />
<figcaption>iOS uses a screenshot of your web app if you don't provide an icon.</figcaption>
</figure>
<p>The standard way to provide an icon is to fill out the <code>icons</code> array in the manifest.json like in the example manifest above. Unfortunately, iOS (even the latest iOS13 public beta) does not respect this value, so for iOS you need to add the "apple-touch-icon" link in your HTML:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token comment"><!-- other stuff in head --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>apple-touch-icon<span class="token punctuation">"</span></span> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>180x180<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./icons/logo180.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<h3>4. Service Worker</h3>
<p>There is a lot that could be written about how to use service workers... but let's keep it simple for now. Let's just set it up so your web app will serve up the "shell" of the app from cache every time, and check for any updates in the background (so the app is up-to-date on the next launch).</p>
<p>I'm always wary of relying too much on 3rd party libraries in my apps, but in this situation I think <a href="https://developers.google.com/web/tools/workbox/">workbox</a> is a great solution.</p>
<blockquote>
<p>If you want to see a hand-written service worker take a look at <a href="https://github.com/wes-goulet/background-noise/commit/8307dfb762f5c676a683705bcceeedcf1387174c#diff-36a6321ccec60e74fcb19c8b57d29b8d">sw.js for Background Noise</a>.</p>
</blockquote>
<p>First register your service worker (on browsers that support it) by putting this little script in your HTML. Notice how we load the service worker after the page has been loaded so it doesn't slow down initial page load.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token string">"serviceWorker"</span> <span class="token keyword">in</span> navigator<span class="token punctuation">)</span> <span class="token punctuation">{</span>
window<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"load"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
navigator<span class="token punctuation">.</span>serviceWorker<span class="token punctuation">.</span><span class="token function">register</span><span class="token punctuation">(</span><span class="token string">"./sw.js"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>html</span><span class="token punctuation">></span></span></code></pre>
<p>Now create a <code>sw.js</code> file:</p>
<pre class="language-js"><code class="language-js"><span class="token function">importScripts</span><span class="token punctuation">(</span>
<span class="token string">"https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>workbox<span class="token punctuation">)</span> <span class="token punctuation">{</span>
workbox<span class="token punctuation">.</span>routing<span class="token punctuation">.</span><span class="token function">registerRoute</span><span class="token punctuation">(</span>
<span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">\.(?:html|js|css|png|jpg|jpeg|svg|gif)$</span><span class="token regex-delimiter">/</span></span><span class="token punctuation">,</span>
<span class="token keyword">new</span> <span class="token class-name">workbox<span class="token punctuation">.</span>strategies<span class="token punctuation">.</span>StaleWhileRevalidate</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<blockquote>
<p>Note the workbox version is hard-coded here. Eventually you may want to check workbox's website for the latest version and see if it's worth updating.</p>
</blockquote>
<p>This file is just saying all <code>html</code>, <code>js</code>, <code>css</code>, and image files will be served from the cache and then updated from the network in the background. You can modify the extensions regex there to capture other files types too (like <code>mp3</code> for audio files). Just be careful about caching <code>json</code>/<code>xml</code> files if your web app fetches those formats from any server endpoints, because then your app would be showing stale data (even when online). Caching such data is out of scope of this post, but maybe I'll dig into it in a future post.</p>
<h2>Example PWA</h2>
<p><a href="https://backgroundnoise.app/">Background Noise</a> is my go-to PWA example. The source can be found <a href="https://github.com/wes-goulet/background-noise">on github</a>. I like to use this as an example because sometimes people think PWAs have to be built as SPAs (single page apps) or built with a nice front-end framework like React, but it is not so. Any website/web app (I tend to use the terms interchangeably) can be enhanced to be a PWA, even a vanilla HTML/CSS/JS web app.</p>
How to Build a PWA without a Build Step
No bundler? No problem.
2019-11-10T00:00:00Z
https://goulet.dev/posts/build-a-pwa-without-a-build-step/
<p><img src="https://goulet.dev/posts/build-a-pwa-without-a-build-step/bundle.jpeg" alt="A bundled notebook" /></p>
<p>Sometimes modern web development feels overly complex. When I build a static website I mostly write HTML and CSS, with a little bit of JavaScript. My project structure is straightforward and boring - just the way I like it.</p>
<p>But when I move beyond "static website" into "web app" or Progressive Web App (PWA) territory then things are no longer simple... Babel config files, TypeScript config files, Webpack or Rollup config files. Make a change, wait for it to build, then try and debug one giant <code>bundle.js</code> file. Surely there's a better way, right?</p>
<h2>From Create React App to a "Buildless" PWA</h2>
<p><img src="https://goulet.dev/posts/build-a-pwa-without-a-build-step/fot.png" alt="Focus On This PWA" /></p>
<p>To figure out if building a PWA without any build step is feasible (enjoyable even?) I took an existing PWA that I had written using <a href="https://github.com/facebook/create-react-app">Create-React-App</a> and converted it to be buildless. The app is rather simple, so I wouldn't say this is an exhaustive experiment, but it was revealing enough to identify some benefits and challenges with building a modern web app without a build step.</p>
<h2>Component <s>Framework</s></h2>
<p><img src="https://goulet.dev/posts/build-a-pwa-without-a-build-step/i-heart-webcomponents.png" alt="I heart webcomponents" /></p>
<p>The original version of my PWA was written with React using JSX. If I want to go buildless I can't use JSX since browsers can't directly render it. Moving away from JSX is fine with me because nowadays I prefer building web components over using any other component framework (like React). Someday I'll write a post about why I like web components so much, but it boils down to this: <em>in general</em>, I prefer something standards-based over a non-standard library/framework.</p>
<p>Great, so I'm going to build web components... but how? There are many libraries/tools to help build web components, and the ones I'm most familiar with are:</p>
<ul>
<li><a href="https://lwc.dev/">Lightning Web Components (LWC)</a></li>
<li><a href="https://lit-element.polymer-project.org/">LitElement</a></li>
<li><a href="https://stenciljs.com/">Stencil</a></li>
</ul>
<p>I think any of these are great options. However, Stencil and LWC have compilers to build their web components whereas LitElement is a base class that can run directly in the browser... sounds perfect if I'm going buildless.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// this runs in the browser, no build step needed πππ</span>
<span class="token keyword">class</span> <span class="token class-name">FotApp</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span>
<span class="token operator">...</span>
<span class="token punctuation">}</span></code></pre>
<h3>Vanilla Web Components</h3>
<p>One last comment regarding web components - I try to write any components that are commonly used across projects as standalone packages and build them without any frameworks/libraries/tools. Examples of these commonly used "base components" are <a href="https://github.com/wes-goulet/side-drawer">side-drawer</a> and <a href="https://github.com/wes-goulet/wc-menu-button">wc-menu-button</a>. The reason I like to write base components with vanilla JS is I can build them without any dependencies and be sure they are as small/lean as possible. You might think the development experience is painful using vanilla JS but ideally your base components are mostly just HTML/CSS, with a little bit of JS. Even if that little bit of JS is fairly boilerplate/low-level then I don't mind π€·ββοΈ.</p>
<h2>Routing</h2>
<p>My original version was using client-side routing via React Router. I could pull in a different client-side router that works with web components, or I could just simplify and not use client-side routing. No SPA? No problem thanks to service worker pre-caching. My service worker can pre-cache all my routes so when a user navigates to a new page it's instantly pulled from the local cache. And soon I can make page transitions even fancier/native-feeling while still keeping the simplicity of server-side routing thanks to <a href="https://drafts.csswg.org/css-view-transitions-2/">View Transitions</a>.</p>
<h2>Type Checking</h2>
<p>I love TypeScript and I figured this would be the deal breaker when it came to going buildless. I don't want to write un-typed JavaScript code like some savage π. But the <code>.ts</code> files won't just magically work in the browser, they have to be run through tsc or babel to strip types, right? That's true, but thankfully TypeScript has this really great feature where you can <a href="http://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html">type check your JavaScript files</a> via JSDoc-style comments. Much to my surprise, it all works pretty well (and <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#--declaration-and---allowjs">it keeps getting better</a>)! πͺπͺπͺ</p>
<h3>Setup</h3>
<p>The first step to setting up type checking in JS code is to create a <code>tsconfig.json</code> file at my repo root.</p>
<blockquote>
<p>As I found out, buildless doesn't necessarily mean you can escape config files π</p>
</blockquote>
<p>Then I set <code>allowJs</code> and <code>checkJs</code> to true. If I didn't want to type check every file I can set <code>checkJs</code> to false and then at the top of the files I do want to check I just include the comment <code>// @ts-check</code>. This is also a great way to slowly move a <a href="https://en.wikipedia.org/wiki/Brownfield_(software_development)">brownfield project</a> to TypeScript.</p>
<h3>Declaring Types</h3>
<p>I won't regurgitate everything in the <a href="http://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html">TypeScript handbook</a>, but I will call out a few things I learned.</p>
<p>To declare an object's type I just use a JSDoc-style comment:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @type {number} */</span>
<span class="token keyword">const</span> length<span class="token punctuation">;</span></code></pre>
<p>To import type declarations from 3rd party libs in node_modules I just <code>import</code> them:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @typedef { import("side-drawer").SideDrawer } SideDrawer
* @typedef { import("lit-element").LitElement } LitElement
*/</span></code></pre>
<blockquote>
<p>In this example I'm defining a <code>@typedef</code> so I don't have to keep using <code>import("lit-element").LitElement</code> every time I need that type in code.</p>
</blockquote>
<p>To declare that my custom element extends from LitElement I then use that imported typedef:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @extends {LitElement} */</span>
<span class="token keyword">class</span> <span class="token class-name">FotDrawer</span> <span class="token keyword">extends</span> <span class="token class-name">LitElement</span> <span class="token punctuation">{</span><span class="token operator">...</span><span class="token punctuation">}</span></code></pre>
<p>It's not quite as quick/easy/natural as setting types in <code>.ts</code> files, but I want type checking without a build step so it works π</p>
<h2>Dependencies</h2>
<p>As I said earlier, I don't want my own code to be put into one big <code>bundle.js</code> file. However, I <em>do</em> want each of my app's dependencies to be bundled (not all dependencies as one bundle but one bundle per dependency). I don't have control of the project structure of my dependencies, and my dependencies shouldn't change very often. So how do I bundle my dependencies without a build step?</p>
<p>Using <a href="https://pika.dev/web">pika web</a> I can have my dependencies bundled as ES modules at install time. I simply set the npm <code>prepare</code> script in my <code>package.json</code></p>
<pre class="language-json"><code class="language-json"><span class="token property">"prepare"</span><span class="token operator">:</span> <span class="token string">"pika-web --clean --strict --dest public/web_modules/"</span><span class="token punctuation">,</span></code></pre>
<p>I also declare which dependencies are to be bundled as web modules in <code>package.json</code>:</p>
<pre class="language-json"><code class="language-json"><span class="token property">"@pika/web"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token property">"webDependencies"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
<span class="token string">"lit-html"</span><span class="token punctuation">,</span>
<span class="token string">"lit-element"</span><span class="token punctuation">,</span>
<span class="token string">"side-drawer"</span>
<span class="token punctuation">]</span>
<span class="token punctuation">}</span></code></pre>
<p>Now any time I run <code>npm install</code> my dependencies will be bundled and copied into the <code>web_modules</code> folder, and then my code can just directly import from the module in that folder:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">import</span> <span class="token punctuation">{</span> LitElement<span class="token punctuation">,</span> html<span class="token punctuation">,</span> css <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"../../web_modules/lit-element.js"</span><span class="token punctuation">;</span></code></pre>
<p>Notice how the import uses a relative path to the actual JS file? No fancy module resolution here. I'm all for abstracting things when it makes sense, but I find this form of "module resolution" refreshingly simple π.</p>
<h2>Pros</h2>
<p>There is a lot I like about switching my web app over to be buildless:</p>
<ul>
<li>Fewer config files</li>
<li>Simpler project structure</li>
<li>Changes are instant during development</li>
<li>What I develop is exactly what I ship</li>
<li>I had less conflicts between build tools</li>
</ul>
<p>Ironically, I am currently having trouble getting the React version of my app to build because one of the dependencies somewhere down in the transitive dependency tree is causing <code>tsc</code> to error out. It's probably not too hard to figure out what the issue is but it does highlight the fact that, <em>in general</em>, the more dependencies and configurations my apps have, the more potential for issues to crop up.</p>
<h2>Cons</h2>
<p>It's not all roses, though. Code bundlers weren't created just to add complexity, after all. They have real benefits. As with most things, there is nuance and trade-off. Here are some issues I noticed when switching over to buildless:</p>
<ul>
<li>There is no tree-shaking.
<ul>
<li>One of my favorite features of Rollup is knowing when I bundle my app all the extra code that might be in my own code or in my dependencies gets shaken out and only what is needed is shipped to my users. Without a build step this doesn't happen π.</li>
<li>In theory the @pika/web tool could fix this, if it provided a way to explicitly say which exports you will use and then it could tree-shake out the rest when it bundles at install time. But that would be pretty tedious to have to declare which exports you plan to use so maybe it's not a great idea, I'm not sure yet.</li>
</ul>
</li>
<li>I can't leverage templates in HTML.
<ul>
<li>Example: All of my pages have the same content in <code><head></code> but I just have to copy/paste that shared content.</li>
<li>With a build step I could leverage a template library like <a href="https://handlebarsjs.com/">handlebars</a>.</li>
</ul>
</li>
<li>I can't leverage <a href="https://developers.google.com/web/tools/workbox/modules/workbox-build">workbox build</a> to generate my service worker without a build step.
<ul>
<li>I can still hand-write a service worker, but it does get tedious and error-prone to remember to add each individual file to the cache list.</li>
</ul>
</li>
<li>It's easier/faster for me to just write TypeScript in <code>.ts</code> files than having JSDoc comments all over the <code>.js</code> files.
<ul>
<li>This might just be a matter of what I'm used to though... and it's not the worst thing to get in the habit of writing JSDoc comments in my <code>.js</code> files.</li>
</ul>
</li>
<li>I can't remove comments or minify without a build step.
<ul>
<li>And since I'm using JSDoc comments for type-checking there are a lot of comments. Comments shouldn't affect JS parse time but they do affect download time so that's not great. Ultimately it depends on the app and target audience to figure out if the extra time it takes to pre-cache my files is acceptable.</li>
</ul>
</li>
<li>I can't support IE11 without a build step to transpile down to pre-ES6.</li>
</ul>
<h2>Final Verdict</h2>
<p>I like the developer experience "pros" of not having a build step, but I don't like the production "cons"... particularly around tree-shaking and minifying. I also really missed workbox build.</p>
<p>I think some sort of middle-ground might be what I try next. My thought is I will basically go buildless during development, and then have a build step for staging and production sites. That "build step" will run workbox build and minify each file. I still won't have tree-shaking but if I am thoughtful about what dependencies I pull in (which I should always be thoughtful about that, bundle or no bundle) then maybe it won't be a big deal. I'll have to do some benchmarking to be sure, but this idea feels like a sort of healthy middle ground of simplicity and build-step benefits.</p>
How To Set Windows Terminal Starting Directory for WSL
2019-11-17T00:00:00Z
https://goulet.dev/posts/how-to-set-windows-terminal-starting-directory/
<p>Now that <a href="https://blogs.windows.com/windowsexperience/2019/10/29/announcing-windows-10-insider-preview-build-19013/">Windows 10 Slow Ring builds have WSL2</a> I've been doing more dev work on my Windows computer (<a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl">VS Code Remote WSL</a> is really good y'all).</p>
<p>I wanted to give the new <a href="https://github.com/Microsoft/Terminal">Windows Terminal</a> a go and the first thing I noticed is that when I open WSL in windows terminal it defaults to my home folder on Windows <code>/mnt/c/Users/wes</code>. If you are like me you want it to start in your linux home directory <code>/home/wes</code> not your Windows home folder.</p>
<h2>The fix</h2>
<p>To fix it you need to modify the settings.json file of Windows Terminal (which you can open with keyboard shotrcut: <code>Ctrl</code> + <code>,</code>).</p>
<p>Once in the settings json file find the WSL entry and set the <code>startingDirectory</code> to the network path of your WSL home directory:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"guid"</span><span class="token operator">:</span> <span class="token string">"{c6eaf9f4-32a7-5fdc-b5cf-066e43243e40}"</span><span class="token punctuation">,</span>
<span class="token property">"hidden"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"Ubuntu-18.04"</span><span class="token punctuation">,</span>
<span class="token property">"source"</span><span class="token operator">:</span> <span class="token string">"Windows.Terminal.Wsl"</span><span class="token punctuation">,</span>
<span class="token property">"startingDirectory"</span><span class="token operator">:</span> <span class="token string">"//wsl$/Ubuntu-18.04/home/wes/"</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span></code></pre>
<p>Now when you launch WSL in the terminal it will start in good old <code>~</code></p>
<p><img src="https://goulet.dev/posts/how-to-set-windows-terminal-starting-directory/wsl-home-path.png" alt="WSL Home Directory" /></p>
<blockquote>
<p>I've only tested this with WSL2 but I think it's the same for WSL1</p>
</blockquote>
<h3>NOTE: Setting a non-network path won't work</h3>
<p>The Windows Terminal only knows about paths from Windows' perspective, so if you try to put in the home directory path directly it won't work:</p>
<p><img src="https://goulet.dev/posts/how-to-set-windows-terminal-starting-directory/home-path-not-working.png" alt="Home path not set" /></p>
How To Password Protect Your Staging Site on Netlify
2019-11-30T00:00:00Z
https://goulet.dev/posts/how-to-password-protect-staging-on-netlify/
<blockquote>
<p>Disclaimer up front, the method described in this post only works with the <a href="https://www.netlify.com/pricing/">paid Netlify plans</a></p>
</blockquote>
<h2>Short Version</h2>
<p>You can password protect branch deploys on Netlify using a "branchdeploy" header file.</p>
<ol>
<li>
<p>Create a file named <code>_branchdeploy_headers</code> with the following content:</p>
<p>/*
Basic-Auth: username:password123</p>
<blockquote>
<p>Obviously, uhh, you'd want to use a better username and password than that π</p>
</blockquote>
</li>
<li>
<p>Create/update your <code>netlify.toml</code> file to copy and rename the header file to <code>_headers</code> on branch-deploys only:</p>
<pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token table class-name">build</span><span class="token punctuation">]</span>
<span class="token key property">base</span> <span class="token punctuation">=</span> <span class="token string">"."</span>
<span class="token key property">command</span> <span class="token punctuation">=</span> <span class="token string">"npm install && npm run build:prod"</span>
<span class="token key property">publish</span> <span class="token punctuation">=</span> <span class="token string">"dist"</span>
<span class="token punctuation">[</span><span class="token table class-name">context.branch-deploy</span><span class="token punctuation">]</span>
<span class="token key property">command</span> <span class="token punctuation">=</span> <span class="token string">"npm install && npm run build:prod && cp _branchdeploy_headers dist/_headers"</span>
</code></pre>
</li>
</ol>
<p>Now your staging site and any other branch deploys are password protected while prod is still open to the world! πππ</p>
<h2>Long Version</h2>
<p>I'm a huge fan of hosting static websites and PWAs on <a href="https://netlify.com/">Netlify</a>. I love the simplicity of it... just link netlify to your github repo and every push to master automatically deploys your site.</p>
<p>One of my favorite features is custom subdomains per branch. On almost all of my sites I have a <code>staging</code> branch that gets deployed to a "staging" subdomain. For example, the <a href="https://github.com/wes-goulet/background-noise/tree/staging">staging branch of Background Noise</a> points to <a href="https://staging.backgroundnoise.app/">staging.backgroundnoise.app</a>.</p>
<h3>The problems with a "staging" subdomain</h3>
<p>There are 2 problems with having a staging site at a subdomain like that. First, you might not want just anybody perusing around your staging site - seeing what features you are working on and using features that aren't ready. Second, your main (production) site might get penalized in search results because you've got duplicate content on your staging site (unless your staging site is just completely different than your prod site, but that's probably not too often the case).</p>
<p>For the first problem you could choose a more obscure subdomain, but that's security through obscurity and that ain't it. For the second problem you could mess around with robot.txt files... but that doesn't sound fun to me.</p>
<p>No, the ideal fix for both problems is to throw a simple password on your staging site. Lookie-lou's can't see what cool stuff you've got coming and search engines won't crawl your non-prod site.</p>
<h3>Netlify authentication to the rescue</h3>
<blockquote>
<p>Currently Netlify authentication is only <a href="https://www.netlify.com/pricing/">part of the Pro plan</a></p>
</blockquote>
<p>Netlify's Pro plan has a <a href="https://docs.netlify.com/visitor-access/password-protection/#site-wide-protection">site-wide password feature</a>... but that's not exactly what we want since it applies a password to all domains (even the prod/main deploy). The feature we will use is Netlify's basic auth headers. We'll make a header file to password protect all files of a given deploy, but we'll only use it on non-prod deploys ("branch deploys").</p>
<ol>
<li>
<p>Create a file named <code>_branchdeploy_headers</code> with the following content:</p>
<p>/*
Basic-Auth: username:password123</p>
<blockquote>
<p>Obviously, uhh, you'd want to use a better username and password than that π</p>
</blockquote>
</li>
<li>
<p>Create/update your <code>netlify.toml</code> file to copy and rename the header file to <code>_headers</code> on branch-deploys only:</p>
<pre class="language-toml"><code class="language-toml"><span class="token punctuation">[</span><span class="token table class-name">build</span><span class="token punctuation">]</span>
<span class="token key property">base</span> <span class="token punctuation">=</span> <span class="token string">"."</span>
<span class="token key property">command</span> <span class="token punctuation">=</span> <span class="token string">"npm install && npm run build:prod"</span>
<span class="token key property">publish</span> <span class="token punctuation">=</span> <span class="token string">"dist"</span>
<span class="token punctuation">[</span><span class="token table class-name">context.branch-deploy</span><span class="token punctuation">]</span>
<span class="token key property">command</span> <span class="token punctuation">=</span> <span class="token string">"npm install && npm run build:prod && cp _branchdeploy_headers dist/_headers"</span>
</code></pre>
</li>
</ol>
<p>Now your staging site and any other branch deploys are password protected while prod is still open to the world! πππ</p>
A Simple Guide to Setting Up zsh
2020-01-04T00:00:00Z
https://goulet.dev/posts/zsh-guide/
<p><img src="https://goulet.dev/posts/zsh-guide/terminal.png" alt="Terminal with zsh" /></p>
<p>Recently I decided to switch my default shell to zsh. I've been using bash for years because... it's been the default. But macOS Catalina <a href="https://support.apple.com/kb/HT208050">changes the default shell from bash to zsh</a> and I got tired of the warning message popping up every time I opened Terminal.</p>
<p><img src="https://goulet.dev/posts/zsh-guide/zsh-warning.png" alt="macOS Catalina warning" /></p>
<p>So, time to switch to zsh and ditch that warning message π.</p>
<p>Now, I'm not looking for an overly-complex setup here. I'm aiming for a quick and simple setup that results in a fast shell that supports auto-complete and the aliases I'm used to. One other feature I'd like is showing git branch info when in a git directory. Let's get to it!</p>
<h2>Steps</h2>
<h3>1. Set zsh as default shell</h3>
<p>You can switch the default shell to zsh by running:</p>
<pre class="language-shell"><code class="language-shell">chsh <span class="token parameter variable">-s</span> /bin/zsh</code></pre>
<p>I'm not totally sure "chsh" stands for CHangeSHell but at least that's how I remember it π</p>
<blockquote>
<p>You can see a list of all supported shells by running <code>cat /etc/shells</code></p>
</blockquote>
<h3>2. Install "Oh My Zsh"</h3>
<p>The simplest way to get a bunch of great features and keep your setup simple is to install <a href="https://github.com/ohmyzsh/ohmyzsh">Oh My Zsh</a>. To install just run the following command in your terminal:</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">sh</span> <span class="token parameter variable">-c</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh<span class="token variable">)</span></span>"</span></code></pre>
<h3>3. Set the theme</h3>
<p>There are a lot of <a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Themes">built-in themes</a> you can use. I personally use "ys" because it shows me git branch info and the input prompt is on a new line (meaning it's at a consistent location regardless of the directory I'm in, so my eyes always know where to look π).</p>
<p>To use a theme just set ZSH_THEME in <code>.zshrc</code>.</p>
<pre class="language-shell"><code class="language-shell"><span class="token assign-left variable">ZSH_THEME</span><span class="token operator">=</span><span class="token string">"ys"</span></code></pre>
<h3>4. (Recommended) Add plugins</h3>
<p>Oh My Zsh comes with plugin support for things like aliases and command completion. There are a bunch of <a href="https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins">built-in plugins</a> that you can add. Just space-separate the list in the <code>.zshrc</code> file:</p>
<pre class="language-shell"><code class="language-shell"><span class="token assign-left variable">plugins</span><span class="token operator">=</span><span class="token punctuation">(</span>git <span class="token function">npm</span> <span class="token function">docker</span> <span class="token function">docker-compose</span><span class="token punctuation">)</span></code></pre>
<h3>5. (Optional) Add your aliases</h3>
<p>The git plugin in Oh My Zsh provides a <a href="https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git/">bunch of git aliases</a>... however I have some git aliases that are just hard-coded into my fingers so I have to add my own aliases.</p>
<blockquote>
<p>Your custom aliases override the plugin aliases so don't worry about name collisions</p>
</blockquote>
<p>To add your own aliases just go to the bottom of the <code>.zshrc</code> file and add them. If you come from bash you can just copy paste what you have setup in your <code>.bash_aliases</code>/<code>.bashrc</code>/<code>.bash_profile</code>.</p>
<pre class="language-shell"><code class="language-shell"><span class="token comment"># custom aliases</span>
<span class="token builtin class-name">alias</span> <span class="token assign-left variable">repos</span><span class="token operator">=</span><span class="token string">"cd ~/Source/Repos"</span>
<span class="token builtin class-name">alias</span> <span class="token assign-left variable">gs</span><span class="token operator">=</span><span class="token string">'git status'</span>
<span class="token builtin class-name">alias</span> <span class="token assign-left variable">ga</span><span class="token operator">=</span><span class="token string">'git add --all'</span>
<span class="token builtin class-name">alias</span> <span class="token assign-left variable">gf</span><span class="token operator">=</span><span class="token string">'git fetch --all --prune'</span>
<span class="token builtin class-name">alias</span> <span class="token assign-left variable">gsu</span><span class="token operator">=</span><span class="token string">'git submodule sync && git submodule update --init --recursive'</span>
<span class="token builtin class-name">alias</span> <span class="token assign-left variable">gc</span><span class="token operator">=</span><span class="token string">'git commit -m'</span></code></pre>
<h2>If coming from an existing shell...</h2>
<p>Some tools add exports to your shell's profile for the tool to work properly. For me, my existing <code>.bash_profile</code> has exports for <a href="https://github.com/nvm-sh/nvm">nvm</a> and gpg, so I need to copy those over to the bottom of my <code>.zshrc</code>.</p>
<pre class="language-shell"><code class="language-shell"><span class="token comment"># gpg needs this</span>
<span class="token assign-left variable">GPG_TTY</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">tty</span><span class="token variable">)</span></span>
<span class="token builtin class-name">export</span> GPG_TTY
<span class="token comment"># nvm needs this</span>
<span class="token builtin class-name">export</span> <span class="token assign-left variable">NVM_DIR</span><span class="token operator">=</span><span class="token string">"<span class="token environment constant">$HOME</span>/.nvm"</span>
<span class="token punctuation">[</span> <span class="token parameter variable">-s</span> <span class="token string">"<span class="token variable">$NVM_DIR</span>/nvm.sh"</span> <span class="token punctuation">]</span> <span class="token operator">&&</span> <span class="token punctuation">\</span>. <span class="token string">"<span class="token variable">$NVM_DIR</span>/nvm.sh"</span> <span class="token comment"># This loads nvm</span>
<span class="token punctuation">[</span> <span class="token parameter variable">-s</span> <span class="token string">"<span class="token variable">$NVM_DIR</span>/bash_completion"</span> <span class="token punctuation">]</span> <span class="token operator">&&</span> <span class="token punctuation">\</span>. <span class="token string">"<span class="token variable">$NVM_DIR</span>/bash_completion"</span> <span class="token comment"># This loads nvm bash_completion</span></code></pre>
<h2>Reference .zshrc</h2>
<p>This is mostly for my own future reference, but you can find <a href="https://gist.github.com/wes-goulet/66553904486fb386fb8de0cadcc700fa">my current <code>.zshrc</code> file here</a>.</p>
Four Reasons For Having a Blog
2020-07-05T00:00:00Z
https://goulet.dev/posts/why-have-a-dev-blog/
<p><img src="https://goulet.dev/posts/why-have-a-dev-blog/write-blog-post.png" alt="Blog Post" /></p>
<p>Why should you, a busy developer, put time and energy into having your own blog?</p>
<ul>
<li>
<p>Writing enforces understanding.</p>
</li>
<li>
<p>There's satisfaction in "shipping" each post.</p>
</li>
<li>
<p>You can reference your own posts when you need to remember how to do something later on.</p>
</li>
<li>
<p>Writing well is like a developer superpower, your blog is where you practice and get the reps in. πͺ</p>
</li>
</ul>
<p><strong>Not every post needs to be long</strong>. You're probably glad this post is already over.</p>
<p><em>Header image from <a href="https://undraw.co/">undraw</a></em></p>
iOS 14 PWA Improvements
2020-07-12T00:00:00Z
https://goulet.dev/posts/ios14-pwa-improvements/
<p><img src="https://goulet.dev/posts/ios14-pwa-improvements/ios14.png" alt="iOS 14 + PWA" /></p>
<p>I recently installed the iOS 14 Beta 2 looking to see what improvements are in store for PWAs. Unfortunately, there aren't really any PWA-specific improvements (ie: features related to service workers or the experience of standalone web apps pinned to the home screen).</p>
<p>The good news is there are some important standards/features being implemented in Safari that a PWA (or any website/webapp) can use.</p>
<h2>Improvements</h2>
<h4>Face ID and Touch ID for the Web</h4>
<p>Safari's implementation of the webauthn API will support Face ID and Touch ID, so now a PWA can have that same seemless authentication experience that native apps have.</p>
<h4>Resize Observer</h4>
<p>Useful for building responsive elements, ResizeObserver can now be used on iOS without any polyfill, resulting in a smaller PWA bundle size. π</p>
<h4>CSS Shadow Parts</h4>
<p>I use web components in my PWAs so this improvement is exciting to me. This standard helps with styling web components. Now that this standard is implemented in all major evergreen browsers (Chrome, Edge, Firefox, Safari) I hope to see more web components leveraging shadow parts for style customization.</p>
<blockquote>
<p>CSS Shadow Parts are actually already implemented in iOS 13.1</p>
</blockquote>
<h4>HTML <code>enterkeyhint</code> attribute</h4>
<p>Native apps have been able to set the enter key label on the virtual keyboard for a while, but now PWAs can do the same using the <code>enterkeyhint</code> attribute.</p>
<p><img src="https://goulet.dev/posts/ios14-pwa-improvements/enterkeyhint.png" alt="enterkeyhint example" /></p>
<h4>WebP Image Format Support</h4>
<p>WebP is a modern image format that supports both lossless and lossy compression. WebP images are smaller than PNGs and JPEGs, resulting in a smaller PWA bundle size. π</p>
<h2>PWA-Specific Features Still Not Implemented</h2>
<p>Unfortunately, there are some PWA features that still don't work on iOS 14. This isn't an exhaustive list, just a couple of things I've noticed so far.</p>
<h4>Background Audio</h4>
<p><s>Like last year, the first thing I checked when I installed iOS 14 was background audio because <a href="https://backgroundnoise.app/">some PWAs</a> really need it. This time around I was hopeful because there was some activity earlier this year on <a href="https://bugs.webkit.org/show_bug.cgi?id=198277">the webkit bug</a>. Unfortunately, background audio still stops as soon as you leave the PWA.</s></p>
<blockquote>
<p><s>Do me a favor and go comment on <a href="https://bugs.webkit.org/show_bug.cgi?id=198277">the webkit bug</a> or <a href="https://twitter.com/wes_goulet/status/1280873047700828160">retweet this</a> to help get this bug prioritized by the Webkit team.</s></p>
</blockquote>
<p>It's fixed as of iOS 15.4! See <a href="https://twitter.com/wes_goulet/status/1491238784020905986?s=20">this tweet</a>.</p>
<h4>Manifest.json Icon Ignored</h4>
<p>The standard way to define icons for a PWA is to set the <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/icons">icons object in manifest.json</a>. Unfortunately iOS ignores that so if you want your PWA icon to show up properly on the user's home screen you will still need to add the following to your HTML:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>link</span> <span class="token attr-name">rel</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>apple-touch-icon<span class="token punctuation">"</span></span> <span class="token attr-name">sizes</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>192x192<span class="token punctuation">"</span></span> <span class="token attr-name">href</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>./logo.png<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
<p>That's easy enough, but it would be nice if you only had to declare your icons in one place and it just works for Android <em>and</em> iOS (and Windows and Linux, etc, etc).</p>
<h4>Push Notifications</h4>
<p>It's hard for a PWA to rival a native app in terms of user experience without something as basic a push notifications, so it's disappointing to see this still isn't implemented on iOS 14.</p>
<h4>BeforeInstallPromptEvent API</h4>
<p>This API allows web apps to programmatically pin the web app to the user's home screen as a PWA. This is a non-standard API so it's unlikely for Apple to implement it, <strong>but it would be nice if there were a programmatic way to add to homescreen</strong>. Having a button the user can tap to install the current site as a PWA is a lot nicer experience than asking the user to dig into the share menu in Safari. Also, "Add to Home Screen" is only available in Safari, not in 3rd party browsers.</p>
<h4>PWAs in App Library</h4>
<p>iOS 14 introduces the concept of App Libraries to automatically group apps together. What I've noticed so far is any PWAs I add to my home screen don't show up in the "Recently Added" or "Suggestions" app libraries. In fact, all I've used so far since installing iOS 14 are the Twitter and Background Noise PWAs and yet they don't show up anywhere in my App Library.</p>
<p><img src="https://goulet.dev/posts/ios14-pwa-improvements/app-library.png" alt="No PWAs in App Library" /></p>
<p>It would be nice if PWAs got the same treatment as native apps for such a visible OS feature.</p>
<h2>Wrapup</h2>
<p>There are still some major roadblocks on iOS that prevent PWAs from having parity with native apps when it comes to user experience and OS integration. Developing PWAs for iOS can feel pretty frustrating when compared to Android and Windows. However, I appreciate that Safari is steadily implementing more web standards. I'll try and keep this post updated with my findings as new iOS 14 betas get released.</p>
<h3>References</h3>
<ul>
<li><a href="https://developer.apple.com/videos/play/wwdc2020/10663">WWDC Web Developers Session</a></li>
<li><a href="https://developer.apple.com/videos/play/wwdc2020/10670">WWDC Face ID and Touch ID for the web</a></li>
<li><a href="https://developers.google.com/speed/webp">WebP Image Format</a></li>
</ul>
Type Guards in Javascript Using JSDoc Comments
2020-10-10T00:00:00Z
https://goulet.dev/posts/type-guard-in-jsdoc/
<p>In Typescript you can write <a href="https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types">type guards</a> to filter down a union type to a single type. For example:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">// user-defined type guard</span>
<span class="token keyword">function</span> <span class="token function">isFish</span><span class="token punctuation">(</span><span class="token parameter"><span class="token literal-property property">pet</span><span class="token operator">:</span> Fish <span class="token operator">|</span> Bird</span><span class="token punctuation">)</span><span class="token operator">:</span> pet is Fish <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token string">"swim"</span> <span class="token keyword">in</span> pet<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> <span class="token literal-property property">pet</span><span class="token operator">:</span> Fish <span class="token operator">|</span> Bird <span class="token operator">=</span> <span class="token function">getPet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// at this point you either have a Fish or Bird</span>
<span class="token keyword">if</span><span class="token punctuation">(</span><span class="token function">isFish</span><span class="token punctuation">(</span>pet<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// at this point you (and tsc and intellisense) know you have a Fish</span>
pet<span class="token punctuation">.</span><span class="token function">swim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token comment">// at this point you (and tsc and intellisense) know you have a Bird</span>
pet<span class="token punctuation">.</span><span class="token function">fly</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h2>JSDoc Type-Checking Version</h2>
<p>What if you write your code in Javascript and use <a href="https://goulet.dev/posts/build-a-pwa-without-a-build-step/#type-checking">JSDoc comments for type-checking and intellisense</a>? You can still write and use type guards!</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @typedef {{swim: () => void}} Fish */</span>
<span class="token comment">/** @typedef {{fly: () => void}} Bird */</span>
<span class="token comment">/**
* @param {Fish | Bird} pet
* @returns {pet is Fish}
*/</span>
<span class="token keyword">function</span> <span class="token function">isFish</span><span class="token punctuation">(</span><span class="token parameter">pet</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token string">"swim"</span> <span class="token keyword">in</span> pet<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">/** @type {Fish | Bird} */</span>
<span class="token keyword">let</span> pet <span class="token operator">=</span> <span class="token function">getPet</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// at this point "pet" is either a Fish or Bird</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">isFish</span><span class="token punctuation">(</span>pet<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// at this point you (and tsc and intellisense) know you have a Fish</span>
pet<span class="token punctuation">.</span><span class="token function">swim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
<span class="token comment">// at this point you (and tsc and intellisense) know you have a Bird</span>
pet<span class="token punctuation">.</span><span class="token function">fly</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
Detect if a PWA is Visible with the Page Visibility API
2020-11-07T00:00:00Z
https://goulet.dev/posts/detect-pwa-visibility/
<p>Need to figure out when your PWA comes to the foreground? The modern web has you covered with the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API">Page Visibility API</a>.</p>
<pre class="language-js"><code class="language-js">document<span class="token punctuation">.</span><span class="token function">addEventListener</span><span class="token punctuation">(</span><span class="token string">"visibilitychange"</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> state <span class="token operator">=</span> document<span class="token punctuation">.</span>visibilityState<span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>state <span class="token operator">===</span> <span class="token string">"hidden"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// your PWA is now in the background</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>state <span class="token operator">===</span> <span class="token string">"visible"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// your PWA is now in the foreground</span>
<span class="token function">refreshData</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<blockquote>
<p>Note: this API isn't specific to PWAs, it works for non-PWA websites/webapps as well.</p>
</blockquote>
<p>Most documentation talks about how this event fires when switching tabs, but this also fires when a PWA is foregrounded/backgrounded.</p>
<p>Try it out yourself by going to <a href="https://visibility-api.netlify.app/">https://visibility-api.netlify.app/</a> and adding it to your home screen.</p>
How To Install Node on macOS with Apple Silicon
Want to use Node on your fancy new MacBook with an M1 (aka Apple Silicon, aka ARM) processor? Sure you do. While macOS Big Sur supports x86 emulation via Rosetta 2, let's run native arm64 binaries for maximum speed and minimum power consumption.
2020-12-10T00:00:00Z
https://goulet.dev/posts/how-to-install-arm-node-on-apple-silicon/
<h2>Step 1: Install arm64 Homebrew</h2>
<p>Open a Terminal and veryify you are not in x86 emulation mode:</p>
<pre class="language-shell"><code class="language-shell">arch</code></pre>
<p>You should see <code>arm64</code>.</p>
<p>Now, we're going to follow <a href="https://docs.brew.sh/Installation">homebrew's recommendation</a> and install homebrew to <code>/opt/homebrew</code>. Below are the commands to create that folder, set the perms of that folder, then install homebrew and add it to your path.</p>
<pre class="language-shell"><code class="language-shell"><span class="token builtin class-name">cd</span> /opt
<span class="token function">sudo</span> <span class="token function">mkdir</span> homebrew
<span class="token function">sudo</span> <span class="token function">chown</span> <span class="token parameter variable">-R</span> <span class="token variable"><span class="token variable">$(</span><span class="token function">whoami</span><span class="token variable">)</span></span> homebrew
<span class="token function">curl</span> <span class="token parameter variable">-L</span> https://github.com/Homebrew/brew/tarball/master <span class="token operator">|</span> <span class="token function">tar</span> xz <span class="token parameter variable">--strip</span> <span class="token number">1</span> <span class="token parameter variable">-C</span> homebrew
<span class="token builtin class-name">echo</span> <span class="token string">"export PATH=/opt/homebrew/bin:<span class="token environment constant">$PATH</span>"</span> <span class="token operator">>></span> ~/.zshrc</code></pre>
<blockquote>
<p>If you don't use zsh as your shell then adjust that last command accordingly</p>
</blockquote>
<p>Now either open a new terminal with your updated <code>.zshrc</code> or source it (<code>source ~/.zshrc</code>) so you can run the <code>brew</code> command from anywhere in your terminal.</p>
<h2>Step 2: Install Node via Homebrew</h2>
<p>This is the easy part... since our homebrew install is arm64 any bottles installed from it are also arm64. So to install the arm64 version of node just run:</p>
<pre class="language-shell"><code class="language-shell">brew <span class="token function">install</span> <span class="token function">node</span></code></pre>
<p>That's it, happy dev'ing on ARM!</p>
Getting Started as a Software Developer
2021-03-07T00:00:00Z
https://goulet.dev/posts/getting-started-as-a-dev/
<p>Someone recently asked me for advice around getting started as a software developer. I started writing a response, then realized I could turn it into a blog post (<a href="https://blog.jonudell.net/2007/04/10/too-busy-to-blog-count-your-keystrokes/">conserve them keystrokes</a>), so here we are.</p>
<p>This advice isn't from a place of "this is what I did to get started" (although I am continuously trying to get better at all these things). This is more like "if I was interviewing someone for a software dev position these are the things I'd be excited to see them doing."</p>
<p>This isn't meant to be absolute advice, there are many paths to an enjoyable career in software development. Take whatever is helpful for you in your unique situation, leave the rest. Onward and upward.</p>
<h2>Summary</h2>
<ul>
<li>Ship
<ul>
<li>Blog posts, apps, components, pull requests, whatever</li>
<li>You don't need permission from anyone</li>
</ul>
</li>
<li>Write
<ul>
<li>Forces you to organize thoughts</li>
<li>Shares your thoughts with others</li>
</ul>
</li>
<li>Blog
<ul>
<li>Combines writing and shipping</li>
<li>Future you can reference your own posts</li>
</ul>
</li>
<li>Contribute to OSS
<ul>
<li>Another opportunity to "ship"</li>
<li>Code reviews are like free training from domain experts</li>
<li>Helps others</li>
<li>Grows your network</li>
</ul>
</li>
<li>Form Habits
<ul>
<li>Motivation fades, habits stick</li>
<li>Establish healthy habits/boundaries around work and side-work</li>
</ul>
</li>
</ul>
<h2>Ship</h2>
<p>When I worked at Microsoft there was this phrase I kept hearing: "always be shipping". Ironically, I didn't really appreciate that idea until I started working on side projects. <strong>You don't need to be at a big company (or any company) to ship.</strong></p>
<p>You can write an app for the open web, host it on a CDN, tell all your friends and family on whatever social media platform you want, and experience the satisfaction and struggle of shipping something - all on your own, without permission from anyone.</p>
<p>It doesn't have to be an app either. It can be a blog post, a simple component, a small pull request to an open-source project... whatever, doesn't matter. The point is to put something out there, get feedback, iterate, and figure out what to ship next. Get the reps in on shipping and you'll gain invaluable experience.</p>
<h2>Write</h2>
<p>You have unique thoughts and ideas. Sharing them with others in written form lets others take their time to understand and build on those ideas. Writing lets you asynchronously coordinate with colleagues without requiring time-sucking synchronous meetings.</p>
<p>Not currently working with other people? Writing is still worth your while. Writing down your thoughts naturally helps weed out the bad ones: it's hard to clearly write out a half-baked thought. Writing helps you solidify your ideas and understandings.</p>
<p>Also, writing helps your future self. A well-written README allows me to almost immediately gain context when I come back to an old project I haven't touched in a while. If there is no README (or a poorly-written/sparse README) it takes a while to pick that project back up, and I don't trust it as much. Similarly, if I come across an open-source project without a good README or good documentation I'm less likely to consider using it or trusting it. Good writing instills confidence, both for others and for your future self.</p>
<h2>Blog</h2>
<p>A blog combines "shipping" with "writing", so it's kind of a win-win. And it provides value to others (a win-win-win).</p>
<p>A blog is your own personal learning archive - if you forget how to do something but you've written it down you can search your own blog on how you did it.</p>
<p>If you have a blog that you write to on any sort of rhythm then you'll be a better writer and shipper. It doesn't have to be that often, once a month, once every other month, doesn't matter. Just some sort of consistency.</p>
<p>And the posts don't have to be long, in fact the shorter the better. Get practice in being clear and concise. Convey the right amount of information that would be valuable, but no more.</p>
<p>Blog about whatever you are learning. That will help solidify your learnings while also providing value to others in the same stage of learning. Think of it like this, if I'm new to <a href="https://git-scm.com/">git</a> a short blog post about "how to change a git commit message" is more helpful than an exhaustive post about "how to use git commit amend". Both posts have their place and purpose, but if you're learning git it's easier to write (and read) the former.</p>
<p>When I'm searching how to solve a technical problem and I don't find an obvious, concise answer amongst the first few results then I will write down the search words I used. Once I figure out the solution to whatever I was searching I will create a blog post with that phrase in the title or description or at least in the first paragraph of the post. That way future folks (including me) who face the same problem will find my post.</p>
<p>An opinionated tip about the blog: keep it simple at first... get the reps in shipping, get it out there, write a few simple posts, then iterate later. You don't need fancy server-side-rendered React components with a multi-megabyte javascript bundle and parallax scrolling effects and whatever else... at least not at first. You don't even need javascript at first. Just find a simple static site generator. I use <a href="https://gohugo.io/">hugo</a> and <a href="https://www.11ty.dev/">eleventy</a> for various static sites, both tools are fairly straightforward to use and produce static sites with excellent performance.</p>
<p>One more opinionated tip: don't put Google Analytics on your blog. Google's tracking code is in enough places on the web, it doesn't need to be on your site too. There are a lot of simple, lightweight, <a href="https://github.com/onurakpolat/awesome-analytics#privacy-focused-analytics">privacy-focused analytics options</a> out there. I use <a href="https://usefathom.com/ref/SYIHQG">fathom analytics</a> and I'm a big fan of its simplicity and focus on privacy. <em>(Note: that's an affiliate link - I'll earn some money if you use that link and you'll get $10 off, <a href="https://usefathom.com/">here</a> is a non-affiliate link if you'd like).</em></p>
<h2>Open Source Contributions</h2>
<p>Contributing to open-source projects (sometimes called "OSS" for open-source software) is another way to "ship" and grow as a software developer. Make a pull request to a project you are interested in and get a code review from an expert in that codebase.</p>
<p>Just remember to read the contribution guidelines (usually in the README or in a CONTRIBUTING.md file) and be patient with maintainers. A lot of popular OSS repos are really only run by a few maintainers and they get a lot of contributions, so it takes a while to sort through all the issues and PRs. And smaller OSS repos are sometimes maintained by folks in their spare time (π).</p>
<p>You can search <a href="https://github.com/topics">topics on GitHub</a> for repos you'd like to contribute to. Look for the "Good First Issue" label in any repo you find. Or check out one of the many sites trying to aggregate Good First Issues, like <a href="https://goodfirstissue.dev/">GoodFirstIssue.dev</a>.</p>
<p>One last tip related to OSS contributions: if there is a company you want to work for (or are about to interview with) then find an open-source repo of theirs and make a contribution. Something like opening an issue for a bug or thoughtful feature request, or making a PR for a doc bug or some small bug fix. Then when you go into the interview you can say you've contributed to one of their projects. If I was interviewing a candidate and they did that then they would immediately stand out in my mind.</p>
<h2>Habits</h2>
<p>I've kind of laid out a lot of things to do in this post, and if you tried to do them all at once it would be pretty overwhelming. Start a blog, practice writing, ship stuff, and contribute to open-source. And maybe you've got a day job and a family and life is overwhelming as it is. That's why I say this isn't absolute advice - take whatever is helpful, leave the rest.</p>
<p>One thing that has been a huge help to me in terms of growing as a software developer (despite having little time/energy margin) is to form habits around some of these things. While motivation might fade, systems (habits) seem to stick.</p>
<p>I don't get particularly jazzed about brushing my teeth and flossing, yet I do those things each night before I go to bed. Habits are powerful like that.</p>
<p>So when it comes to blogging or working on side projects I have a pretty simple habit. Three days a week I wake up an hour early and go right to my computer to either work on a blog post or a side project for an hour. It's not much time per week, but if I stick to it that time adds up over the year. And the amount of learning and growing that happens in those 3-ish hours each week makes me a much better software developer.</p>
<h2>A Quick Thought on <a href="https://en.wikipedia.org/wiki/Impostor_syndrome">Imposter Syndrome</a></h2>
<p><em>"I don't know what I'm doing."</em></p>
<p><em>"These people are so much smarter than me."</em></p>
<p>These are thoughts I have fairly often... and for good reason. A lot of the time, I really don't know what I'm doing. But that means I'm in a good place to learn something new and perhaps bring some fresh perspective to something.</p>
<p>And the people I work with are really smart, while I'm just a run-of-the-mill dude. But that doesn't really matter. I still have unique knowledge and experiences that I can share with others that can be valuable. And so do you.</p>
<p>So if you ever feel like an imposter and you don't know what you're doing, that's ok. You're not alone in feeling that and your knowledge and experiences are still valuable. Keep learning, keep growing, keep creating.</p>
Make Your Resume Stand Out with Highlights
2021-04-17T00:00:00Z
https://goulet.dev/posts/resume-highlights/
<p>Want to make your resume stand out and memorable?</p>
<p>When listing out job/project experience do two things:</p>
<ol>
<li>Be detailed in your bullet points</li>
<li>Call out one interesting story, with details. Call it a "Highlight".</li>
</ol>
<p>Bullets are succinct, stories are memorable.</p>
<h2>Example</h2>
<blockquote>
<h4>Frontend Engineer - Foo Company</h4>
<p><em>2018-2020</em></p>
<ul>
<li>Key member of 5-person dev team</li>
<li>Areas of ownership: client-side caching, logging/instrumentation</li>
<li>As SME (subject matter expert) for logging, I designed and documented our logging infrastructure.</li>
<li>I ran multiple brown-bag learning sessions to teach other engineers best practices for our logging feature.</li>
<li>We shipped site updates every 4 weeks, fixing bugs and delivering customer features</li>
</ul>
<h5>Highlight</h5>
<p>We started seeing customer cases around the checkout experience of the website, saved carts were expiring sooner than they should. I took ownership of the issue and added some additional verbose logging to our shopping cart component. Shortly after deploying the updated component with logging I was able to read the logs and narrow down the issue, only customers in eastern time zones hit the issue. I was able to add a test to reproduce the issue. Once a test was in place I was able to step through the debugger on the test and figure out the bug. Javascript dates assign months with a zero-based index (so January is month 0, not 1) but our code was setting January to month 1. I made the code fix, validating it with both the test and on our staging environment.</p>
</blockquote>
<p>The bullets plus the highlight tell me a few things about you, the candidate:</p>
<ul>
<li>You "owned" certain things at that job, I can delegate work to you on our team and trust you to "own" them</li>
<li>You taught others what you were learning, you can come onto our team and make others on our team better</li>
<li>You know how to ship frequently, you can come onto our team and help us ship</li>
<li>You care about quality (in the highlight story you reproduced the bug with a test, and then TDD'ed the fix... personally if someone did this I would insta-hire them, I am a sucker for TDD)</li>
<li>You were competent enough at logging to figure out the issue. Logging is a tricky thing... you can easily over-log and then logs are just noise and not useful. Or you can under-log and logs don't really tell you anything. If you provide an example where logging helped you then that means you have experience logging the right information.</li>
<li>You can explain technical things, like that bug you fixed, in a succinct and easy-to-understand way. I know that you can come onto our team and communicate issues up to management in an understandable way.</li>
</ul>
<blockquote>
<p>While that example "highlight" is made up, the part about months in Javascript is true, be careful out there. π</p>
</blockquote>
Enabling Emoji Shortcodes in Eleventy Markdown Files
Turn on emoji shortcodes in markdown files in Eleventy by adding the markdwon-it-emoji plugin.
2021-11-24T00:00:00Z
https://goulet.dev/posts/emoji-shortcodes-in-eleventy/
<p>In moving my blog from hugo to eleventy I noticed my emoji shortcodes (ie: writing <code>:fire:</code> and having the π₯ emoji show up in the generated page) weren't working. π¦</p>
<p>Some quick internet searching didn't turn up a direct result (thus this blog post), but I did stumble upon the answer in the <a href="https://www.11ty.dev/docs/languages/markdown/#add-your-own-plugins">eleventy docs around markdown</a>. We need to tell eleventy to use the markdown-it-emoji plugin with a couple simple steps.</p>
<h2>Eleventy 2.0 and later</h2>
<p>Step 1: Install the markdown-it-emoji plugin</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> i <span class="token parameter variable">-D</span> markdown-it-emoji</code></pre>
<p>Step 2: Update <code>.eleventy.js</code> config file and register the emoji plugin</p>
<pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// setup markdown to use emoji plugin</span>
<span class="token keyword">const</span> markdownItEmoji <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"markdown-it-emoji"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">amendLibrary</span><span class="token punctuation">(</span><span class="token string">"md"</span><span class="token punctuation">,</span> <span class="token parameter">mdLib</span> <span class="token operator">=></span> mdLib<span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>markdownItEmoji<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h2>Eleventy 1.0 and earlier</h2>
<p>Step 1: Install markdown-it and the markdown-it-emoji plugin</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> i <span class="token parameter variable">-D</span> markdown-it
<span class="token function">npm</span> i <span class="token parameter variable">-D</span> markdown-it-emoji</code></pre>
<p>Step 2: Update <code>.eleventy.js</code> config file and register the emoji plugin</p>
<pre class="language-js"><code class="language-js">module<span class="token punctuation">.</span><span class="token function-variable function">exports</span> <span class="token operator">=</span> <span class="token keyword">function</span> <span class="token punctuation">(</span><span class="token parameter">eleventyConfig</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// setup markdown to use emoji plugin</span>
<span class="token keyword">const</span> markdownIt <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"markdown-it"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> markdownItEmoji <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">"markdown-it-emoji"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> markdownLib <span class="token operator">=</span> <span class="token function">markdownIt</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">html</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">use</span><span class="token punctuation">(</span>markdownItEmoji<span class="token punctuation">)</span><span class="token punctuation">;</span>
eleventyConfig<span class="token punctuation">.</span><span class="token function">setLibrary</span><span class="token punctuation">(</span><span class="token string">"md"</span><span class="token punctuation">,</span> markdownLib<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>Now emoji shortcodes work! πͺ π₯ π₯</p>
Netlify Functions in Javascript with Type-Checking
2023-03-11T00:00:00Z
https://goulet.dev/posts/netlify-functions-js-typing/
<p><img src="https://goulet.dev/posts/netlify-functions-js-typing/function-in-js-with-typing.png" alt="JS Function with strong-typing" /></p>
<blockquote>
<p>Shout out to <a href="https://eduardoboucas.com/">Eduardo</a> for pointing out that netlify already publishes an npm package with types (I don't need to write my own <code>.d.ts</code> file) πͺ</p>
</blockquote>
<h2>Short Version</h2>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @type {import("@netlify/functions").Handler} */</span>
<span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// all these properties are typed</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> httpMethod<span class="token punctuation">,</span> body<span class="token punctuation">,</span> headers <span class="token punctuation">}</span> <span class="token operator">=</span> event<span class="token punctuation">;</span>
<span class="token comment">// response is typed</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span>
<span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h2>Long Version</h2>
<p>I really like Netlify Functions. β€οΈ</p>
<p>I really like <s>TypeScript</s> modern Javascript with type-checking. π</p>
<p>The appeal of <a href="https://docs.netlify.com/functions/overview/">Netlify Functions</a> is how easy it is to setup: make a file, get a new api endpoint.</p>
<p>Writing code in <code>.ts</code> files makes something that was easy become not quite as easy.</p>
<p>Not quite as easy to get started (step 1: add a tsconfig, step 2: figure out what goes into tsconfig).</p>
<p>Not quite as easy to debug ("Why did the debugger just jump down there, are my source maps messed up?").</p>
<p>It's not too bad, but any config/build ceremony that slows down development should be scrutinized.</p>
<p>I find writing <code>.js</code> files (actually <code>.mjs</code> files, it's 2023, let's embrace ESM) with a combination of <code>.d.ts</code> declaration files and jsdoc comments to be the sweet spot of getting type-checking without any Typescript config or build steps. π</p>
<p>Here is how I setup my Netlify Functions projects.</p>
<h2>Step 1: Install <code>@netlify/functions</code> npm package</h2>
<p>Netlify publishes an NPM package with types for their Functions, add that to your package.json.</p>
<pre class="language-shell"><code class="language-shell"><span class="token function">npm</span> i <span class="token parameter variable">-D</span> @netlify/functions</code></pre>
<h2>Step 2: JSDoc <code>@type</code> the handler</h2>
<p>Now in your Function's JS file you can use a jsdoc comment to apply typing.</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @type {import("@netlify/functions").Handler} */</span>
<span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">handler</span> <span class="token operator">=</span> <span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token parameter">event<span class="token punctuation">,</span> context</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token comment">// all these properties are typed</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> httpMethod<span class="token punctuation">,</span> body<span class="token punctuation">,</span> headers <span class="token punctuation">}</span> <span class="token operator">=</span> event<span class="token punctuation">;</span>
<span class="token comment">// response is typed</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span>
<span class="token literal-property property">statusCode</span><span class="token operator">:</span> <span class="token number">200</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<p>That's it! π</p>
Running Database Webhooks Locally with Supabase CLI
2023-03-11T00:00:00Z
https://goulet.dev/posts/supabase-webhook-local/
<p>Supabase supports database webhooks when running a hosted instance, but I never could get them to work when running Supabase locally via the CLI. I would get an error:</p>
<pre class="language-text"><code class="language-text">Failed to create hook: failed to create pg.triggers: schema "supabase_functions" does not exist</code></pre>
<blockquote>
<p>I just noticed <a href="https://github.com/supabase/supabase/pull/12909">this PR</a> merged in yesterday, maybe this fixes it π€·ββοΈ</p>
</blockquote>
<p>It seems like the DB schema that is setup for local Supabase is not complete compared to hosted Supabase, because the "supabase_functions" schema isn't defined for local Supabase (even if you go into the dashboard UI > Database > Webhooks and click the "Enable Hooks" button).</p>
<p>The way I <s>solved</s> worked around this was making my own custom function that is pretty much a copy/paste of <a href="https://github.com/supabase/supabase/blob/master/docker/volumes/db/webhooks.sql">the official supabase_functions code</a>:</p>
<pre class="language-sql"><code class="language-sql"><span class="token keyword">CREATE</span>
<span class="token operator">OR</span> <span class="token keyword">REPLACE</span> <span class="token keyword">FUNCTION</span> send_to_webhook <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">RETURNS</span> <span class="token keyword">TRIGGER</span> <span class="token keyword">LANGUAGE</span> plpgsql <span class="token keyword">AS</span> $<span class="token keyword">function</span>$
<span class="token keyword">DECLARE</span>
request_id <span class="token keyword">bigint</span><span class="token punctuation">;</span>
payload jsonb<span class="token punctuation">;</span>
url <span class="token keyword">text</span> :<span class="token operator">=</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span>::<span class="token keyword">text</span><span class="token punctuation">;</span>
method <span class="token keyword">text</span> :<span class="token operator">=</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span>::<span class="token keyword">text</span><span class="token punctuation">;</span>
headers jsonb <span class="token keyword">DEFAULT</span> <span class="token string">'{}'</span>::jsonb<span class="token punctuation">;</span>
params jsonb <span class="token keyword">DEFAULT</span> <span class="token string">'{}'</span>::jsonb<span class="token punctuation">;</span>
timeout_ms <span class="token keyword">integer</span> <span class="token keyword">DEFAULT</span> <span class="token number">1000</span><span class="token punctuation">;</span>
<span class="token keyword">BEGIN</span>
<span class="token keyword">IF</span> url <span class="token operator">IS</span> <span class="token boolean">NULL</span> <span class="token operator">OR</span> url <span class="token operator">=</span> <span class="token string">'null'</span> <span class="token keyword">THEN</span>
RAISE EXCEPTION <span class="token string">'url argument is missing'</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> <span class="token keyword">IF</span><span class="token punctuation">;</span>
<span class="token keyword">IF</span> method <span class="token operator">IS</span> <span class="token boolean">NULL</span> <span class="token operator">OR</span> method <span class="token operator">=</span> <span class="token string">'null'</span> <span class="token keyword">THEN</span>
RAISE EXCEPTION <span class="token string">'method argument is missing'</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> <span class="token keyword">IF</span><span class="token punctuation">;</span>
<span class="token keyword">IF</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token operator">IS</span> <span class="token boolean">NULL</span> <span class="token operator">OR</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'null'</span> <span class="token keyword">THEN</span>
headers <span class="token operator">=</span> <span class="token string">'{"Content-Type": "application/json"}'</span>::jsonb<span class="token punctuation">;</span>
<span class="token keyword">ELSE</span>
headers <span class="token operator">=</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">2</span><span class="token punctuation">]</span>::jsonb<span class="token punctuation">;</span>
<span class="token keyword">END</span> <span class="token keyword">IF</span><span class="token punctuation">;</span>
<span class="token keyword">IF</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">]</span> <span class="token operator">IS</span> <span class="token boolean">NULL</span> <span class="token operator">OR</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'null'</span> <span class="token keyword">THEN</span>
params <span class="token operator">=</span> <span class="token string">'{}'</span>::jsonb<span class="token punctuation">;</span>
<span class="token keyword">ELSE</span>
params <span class="token operator">=</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">3</span><span class="token punctuation">]</span>::jsonb<span class="token punctuation">;</span>
<span class="token keyword">END</span> <span class="token keyword">IF</span><span class="token punctuation">;</span>
<span class="token keyword">IF</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">4</span><span class="token punctuation">]</span> <span class="token operator">IS</span> <span class="token boolean">NULL</span> <span class="token operator">OR</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">4</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'null'</span> <span class="token keyword">THEN</span>
timeout_ms <span class="token operator">=</span> <span class="token number">1000</span><span class="token punctuation">;</span>
<span class="token keyword">ELSE</span>
timeout_ms <span class="token operator">=</span> TG_ARGV<span class="token punctuation">[</span><span class="token number">4</span><span class="token punctuation">]</span>::<span class="token keyword">integer</span><span class="token punctuation">;</span>
<span class="token keyword">END</span> <span class="token keyword">IF</span><span class="token punctuation">;</span>
<span class="token keyword">CASE</span>
<span class="token keyword">WHEN</span> method <span class="token operator">=</span> <span class="token string">'GET'</span> <span class="token keyword">THEN</span>
<span class="token keyword">SELECT</span> http_get <span class="token keyword">INTO</span> request_id <span class="token keyword">FROM</span> net<span class="token punctuation">.</span>http_get<span class="token punctuation">(</span>
url<span class="token punctuation">,</span>
params<span class="token punctuation">,</span>
headers<span class="token punctuation">,</span>
timeout_ms
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">WHEN</span> method <span class="token operator">=</span> <span class="token string">'POST'</span> <span class="token keyword">THEN</span>
payload <span class="token operator">=</span> jsonb_build_object<span class="token punctuation">(</span>
<span class="token string">'old_record'</span><span class="token punctuation">,</span> OLD<span class="token punctuation">,</span>
<span class="token string">'record'</span><span class="token punctuation">,</span> NEW<span class="token punctuation">,</span>
<span class="token string">'type'</span><span class="token punctuation">,</span> TG_OP<span class="token punctuation">,</span>
<span class="token string">'table'</span><span class="token punctuation">,</span> TG_TABLE_NAME<span class="token punctuation">,</span>
<span class="token string">'schema'</span><span class="token punctuation">,</span> TG_TABLE_SCHEMA
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">SELECT</span> http_post <span class="token keyword">INTO</span> request_id <span class="token keyword">FROM</span> net<span class="token punctuation">.</span>http_post<span class="token punctuation">(</span>
url<span class="token punctuation">,</span>
payload<span class="token punctuation">,</span>
params<span class="token punctuation">,</span>
headers<span class="token punctuation">,</span>
timeout_ms
<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">ELSE</span>
RAISE EXCEPTION <span class="token string">'method argument % is invalid'</span><span class="token punctuation">,</span> method<span class="token punctuation">;</span>
<span class="token keyword">END</span> <span class="token keyword">CASE</span><span class="token punctuation">;</span>
<span class="token keyword">RETURN</span> NEW<span class="token punctuation">;</span>
<span class="token keyword">END</span>
$<span class="token keyword">function</span>$<span class="token punctuation">;</span></code></pre>
<p>Now I can create a trigger to call that function with something like:</p>
<pre class="language-sql"><code class="language-sql"><span class="token comment">-- now create the trigger</span>
<span class="token keyword">CREATE</span> <span class="token keyword">TRIGGER</span>
<span class="token string">"domains-sync"</span>
<span class="token keyword">AFTER</span>
<span class="token keyword">INSERT</span>
<span class="token operator">OR</span> <span class="token keyword">DELETE</span>
<span class="token operator">OR</span> <span class="token keyword">UPDATE</span>
<span class="token keyword">ON</span> <span class="token keyword">public</span><span class="token punctuation">.</span>domains <span class="token keyword">FOR EACH ROW</span>
<span class="token keyword">EXECUTE</span>
<span class="token keyword">FUNCTION</span> send_to_webhook <span class="token punctuation">(</span>
<span class="token string">'http://host.docker.internal:8888/api/hook-sb-domains'</span><span class="token punctuation">,</span>
<span class="token string">'POST'</span><span class="token punctuation">,</span>
<span class="token string">'{"Content-type":"application/json","x-custom-signature":"xxxCUSTOM_SIGNATURExxx"}'</span><span class="token punctuation">,</span>
<span class="token string">'{}'</span><span class="token punctuation">,</span>
<span class="token string">'1000'</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre>
<p>Notice my trigger calls my docker host (I'm running a netlify function locally to handle my webhook event). I also added a header "x-custom-signature" just to help ensure the calls to my netlify function are really from my Supabase trigger.</p>
<p>If I save this SQL into a migration file I'll need to ensure the webhook address and custom header are updated to the proper environment variables when this gets deployed to production. To do that I leverage a github action to swap in correct values (based on the <a href="https://supabase.com/docs/guides/cli/managing-environments#configure-github-actions">Supabase doc's suggestions here</a>).</p>
<pre class="language-yml"><code class="language-yml"><span class="token key atrule">name</span><span class="token punctuation">:</span> Deploy Migrations to Production
<span class="token key atrule">on</span><span class="token punctuation">:</span>
<span class="token key atrule">push</span><span class="token punctuation">:</span>
<span class="token key atrule">branches</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> main
<span class="token key atrule">workflow_dispatch</span><span class="token punctuation">:</span>
<span class="token key atrule">jobs</span><span class="token punctuation">:</span>
<span class="token key atrule">deploy</span><span class="token punctuation">:</span>
<span class="token key atrule">runs-on</span><span class="token punctuation">:</span> ubuntu<span class="token punctuation">-</span><span class="token number">22.04</span>
<span class="token key atrule">env</span><span class="token punctuation">:</span>
<span class="token key atrule">SUPABASE_ACCESS_TOKEN</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>% raw %<span class="token punctuation">}</span>$<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.SUPABASE_ACCESS_TOKEN <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">{</span>% endraw %<span class="token punctuation">}</span>
<span class="token key atrule">SUPABASE_DB_PASSWORD</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>% raw %<span class="token punctuation">}</span>$<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.PRODUCTION_DB_PASSWORD <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">{</span>% endraw %<span class="token punctuation">}</span>
<span class="token key atrule">PRODUCTION_PROJECT_ID</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>% raw %<span class="token punctuation">}</span>$<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.PRODUCTION_PROJECT_ID <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">{</span>% endraw %<span class="token punctuation">}</span>
<span class="token key atrule">steps</span><span class="token punctuation">:</span>
<span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> actions/checkout@v3
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Replace docker.internal domain with real domain in sql migrations
<span class="token key atrule">uses</span><span class="token punctuation">:</span> jacobtomlinson/gha<span class="token punctuation">-</span>find<span class="token punctuation">-</span>replace@v3
<span class="token key atrule">with</span><span class="token punctuation">:</span>
<span class="token key atrule">find</span><span class="token punctuation">:</span> <span class="token string">"http://host.docker.internal:8888/"</span>
<span class="token key atrule">replace</span><span class="token punctuation">:</span> <span class="token string">"https://my-site.com/"</span>
<span class="token key atrule">include</span><span class="token punctuation">:</span> <span class="token string">"supabase/migrations/*.sql"</span>
<span class="token key atrule">regex</span><span class="token punctuation">:</span> <span class="token boolean important">false</span>
<span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Replace x<span class="token punctuation">-</span>custom<span class="token punctuation">-</span>signature header value with secret one
<span class="token key atrule">uses</span><span class="token punctuation">:</span> jacobtomlinson/gha<span class="token punctuation">-</span>find<span class="token punctuation">-</span>replace@v3
<span class="token key atrule">with</span><span class="token punctuation">:</span>
<span class="token key atrule">find</span><span class="token punctuation">:</span> <span class="token string">"xxxCUSTOM_SIGNATURExxx"</span>
<span class="token key atrule">replace</span><span class="token punctuation">:</span> <span class="token punctuation">{</span>% raw %<span class="token punctuation">}</span>$<span class="token punctuation">{</span><span class="token punctuation">{</span> secrets.SUPABASE_WEBHOOK_CUSTOM_SIGNATURE <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">{</span>% endraw %<span class="token punctuation">}</span>
<span class="token key atrule">include</span><span class="token punctuation">:</span> <span class="token string">"supabase/migrations/*.sql"</span>
<span class="token key atrule">regex</span><span class="token punctuation">:</span> <span class="token boolean important">false</span>
<span class="token punctuation">-</span> <span class="token key atrule">uses</span><span class="token punctuation">:</span> supabase/setup<span class="token punctuation">-</span>cli@v1
<span class="token key atrule">with</span><span class="token punctuation">:</span>
<span class="token key atrule">version</span><span class="token punctuation">:</span> 1.33.0
<span class="token punctuation">-</span> <span class="token key atrule">run</span><span class="token punctuation">:</span> <span class="token punctuation">|</span><span class="token scalar string">
supabase link --project-ref $PRODUCTION_PROJECT_ID
supabase db push</span></code></pre>
<p>Now when I merge to <code>main</code> branch the action will swap in real values before running the migration.</p>
<p>While this hand-coded webhooks code doesn't have the audit trail that Supabase's built-in "supabase_functions" schema has, it's good enough for my needs and works locally. Hopefully someday soon Supabase local development will be fixed to behave just like hosted (making the "supabase_functions" schema when setting up a local environment).</p>
How to Serialize Errors in Javascript
2023-04-30T00:00:00Z
https://goulet.dev/posts/error-serialization-in-js/
<h2>Don't use a simple <code>JSON.stringify</code></h2>
<p>If you want to serialize an Error in Javascript you might be tempted to simply use <code>JSON.stringify</code>, but that won't be very helpful.</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> error <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Something went wrong"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> serialized <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// serialized -> '{}'</span></code></pre>
<h2>Normalize the error</h2>
<p>Instead what I like to do is "normalize" the error (in case it's not actually an <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error"><code>Error</code> object</a>) and then use the error message:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @param {unknown} error
* @returns {Error}
*/</span>
<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">normalizeError</span><span class="token punctuation">(</span><span class="token parameter">error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">typeof</span> error <span class="token operator">===</span> <span class="token string">"object"</span> <span class="token operator">&&</span> error <span class="token keyword">instanceof</span> <span class="token class-name">Error</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> error<span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token keyword">typeof</span> error <span class="token operator">===</span> <span class="token string">"string"</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// else turn this unknown thing into a string</span>
<span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment">// NOW I CAN CATCH ERRORS...</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Something went wrong"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> normalized <span class="token operator">=</span> <span class="token function">normalizeError</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> serialized <span class="token operator">=</span> normalized<span class="token punctuation">.</span>message<span class="token punctuation">;</span>
<span class="token comment">// serialized -> "Something went wrong"</span>
<span class="token punctuation">}</span>
<span class="token comment">// AND ANYTHING ELSE</span>
<span class="token keyword">try</span> <span class="token punctuation">{</span>
<span class="token keyword">throw</span> <span class="token string">"Who throws a string? ME!"</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span>error<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> normalized <span class="token operator">=</span> <span class="token function">normalizeError</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> serialized <span class="token operator">=</span> normalized<span class="token punctuation">.</span>message<span class="token punctuation">;</span>
<span class="token comment">// serialized -> "Who throws a string? ME!"</span>
<span class="token punctuation">}</span></code></pre>
<h2>Alternative: Use overloaded <code>JSON.stringify</code></h2>
<p>If you <em>know</em> you are dealing with an <code>Error</code> object (so you don't need to normalize it) then you can use <code>JSON.stringify</code> with a second argument to tell JSON stringify which properties to enumerate:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> error <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Something went wrong"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> serialized <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>error<span class="token punctuation">,</span> Object<span class="token punctuation">.</span><span class="token function">getOwnPropertyNames</span><span class="token punctuation">(</span>error<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// serialized -> '{"stack":"Error: Something went wrong\\n at REPL9:1:13\\n at Script.runInThisContext (node:vm:131:12)\\n at REPLServer.defaultEval (node:repl:522:29)\\n at bound (node:domain:416:15)\\n at REPLServer.runBound [as eval] (node:domain:427:12)\\n at REPLServer.onLine (node:repl:844:10)\\n at REPLServer.emit (node:events:390:22)\\n at REPLServer.EventEmitter.emit (node:domain:470:12)\\n at REPLServer.Interface._onLine (node:readline:418:10)\\n at REPLServer.Interface._line (node:readline:763:8)","message":"Something went wrong"}'</span></code></pre>
<p>If you don't want the stack in there you can just list an array of the properties you want:</p>
<pre class="language-js"><code class="language-js"><span class="token keyword">const</span> error <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Something went wrong"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> serialized <span class="token operator">=</span> <span class="token constant">JSON</span><span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>error<span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">"message"</span><span class="token punctuation">,</span> <span class="token string">"name"</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// serialized -> '{"message":"Something went wrong", "name": "Error"}'</span></code></pre>
There is an App for That (But Shouldn't Be)
Ditch the burden of building and maintaining a native app. Make your existing website responsive/mobile-friendly and your users will be better served for it.
2023-06-04T00:00:00Z
https://goulet.dev/posts/app-for-that/
<p>I wish there were fewer low-touch apps on the App Store, and more high-quality mobile sites on the open web.</p>
<p>I saw an advertisement on ADP's website for their benefits mobile app. With that app you can compare plan options, add dependents, and select benefits during open enrollment. All things you would do once a year (maybe a few times if you have some life changes in a given year).</p>
<p>So why download an app that you will use once a year? Why does a company make an app with such presumably small usage? Is it so they can say "We have an app in the App Store"?</p>
<p>Ditch the burden of building and maintaining a native app. Make your existing website responsive/mobile-friendly and your users will be better served for it.</p>
theme-color meta tag vs manifest theme_color
Web standards allow for setting the theme color of your site in 2 different ways. Which one should you use? I say use both.
2023-06-25T00:00:00Z
https://goulet.dev/posts/theme-color-meta-tag/
<h2><code>theme-color</code> meta tag</h2>
<p>You can set your site's theme color with the <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color"><code>theme-color</code> meta tag</a> in the <code><head></code> of your site:</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>meta</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>theme-color<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>#4285f4<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span></code></pre>
<p>Browsers will use this color for certain browser UI elements (like the address bar) when the user visits your site.</p>
<blockquote>
<p>If your theme color causes contrast issues it might not be used by some browsers.</p>
</blockquote>
<h2><code>theme_color</code> in web app manifest</h2>
<p>You can also set a <a href="https://developer.mozilla.org/en-US/docs/Web/Manifest/theme_color"><code>theme_color</code> in your site's web app manifest</a>:</p>
<pre class="language-json"><code class="language-json"><span class="token punctuation">{</span>
<span class="token property">"name"</span><span class="token operator">:</span> <span class="token string">"My Site"</span><span class="token punctuation">,</span>
<span class="token property">"theme_color"</span><span class="token operator">:</span> <span class="token string">"#4285f4"</span><span class="token punctuation">,</span>
<span class="token property">"background_color"</span><span class="token operator">:</span> <span class="token string">"#fff"</span>
<span class="token punctuation">}</span></code></pre>
<p>The OS will use this color for certain UI elements when the user installs your site as a PWA. For example, this will set the color of your app's title bar on Windows.</p>
<h2>Use Both</h2>
<p>In my experience, desktop chromium-based browsers (Chrome, Edge, etc) will use the <code>theme-color</code> meta tag over the <code>theme_color</code> in the manifest when setting the color the of the app title bar.</p>
<p>So which should you use? Use both and set them to the same value (as <a href="https://web.dev/add-manifest/#theme-color">mentioned on web.dev</a>).</p>
How to Write TypeScript Interfaces in JSDoc Comments
Just because you are using vanilla .js files doesn't mean you can't use TypeScript interfaces.
2023-07-23T00:00:00Z
https://goulet.dev/posts/how-to-write-ts-interfaces-in-jsdoc/
<p><img src="https://goulet.dev/posts/how-to-write-ts-interfaces-in-jsdoc/defined-in-jsdoc.jpg" alt="Interfaces defined in jsdoc comment" /></p>
<blockquote>
<p>Updated 2023-07-23 to clarify that jsdoc comments support <strong>object types</strong>, not <strong>interfaces</strong>. Thanks to <a href="https://twitter.com/mattpocockuk/status/1682059208412344326?s=20">Matt Pocock</a> for the pointer.</p>
</blockquote>
<p>I like writing web apps without any build step, just vanilla <code>.js</code> files. But I still like the type-checking that TypeScript provides. Thankfully <a href="https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html">TypeScript supports type-checking <code>.js</code> files via JSDoc comments</a>.</p>
<p>But how do you write out interfaces without a <code>.ts</code> file? The tl;dr is you can write an object type in a JSDoc comment, or you can write an interface in a <code>.d.ts</code> file and import that into your <code>.js</code> file. Let's dig into each option.</p>
<h2>Interfaces in a .ts file</h2>
<p>First, let's look at an example in a <code>.ts</code> file.</p>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">interface</span> <span class="token class-name">Address</span> <span class="token punctuation">{</span>
street<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
city<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
zip<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">interface</span> <span class="token class-name">Customer</span> <span class="token punctuation">{</span>
name<span class="token operator">:</span> sting<span class="token punctuation">;</span>
email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
address<span class="token operator">:</span> Address<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h2>Option 1: Object Types in a JSDoc comment</h2>
<p>An <a href="https://www.typescriptlang.org/docs/handbook/2/objects.htm">object type</a> isn't exactly an interface, but for most use cases it behaves the same way.</p>
<blockquote>
<p>The main difference is interfaces support <a href="https://www.typescriptlang.org/docs/handbook/declaration-merging.html">declaration merging</a> but object types do not.</p>
</blockquote>
<p>Writing those same interfaces in a <code>.js</code> file would look like:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @typedef Address
* @prop {string} street The street
* @prop {string} city The City
* @prop {number} zip The zip code
*
* @typedef Customer
* @prop {string} name The Customer's name
* @prop {string} email The Customer's email
* @prop {Address} address The Customer's address
*/</span></code></pre>
<p>And then you can apply that interface to an object using the <code>@type</code> annotation:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/** @type {Customer} */</span>
<span class="token keyword">const</span> theCustomer <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span></code></pre>
<p>Boom π₯ now you've got type-checking and intellisense on your data model right in a vanilla <code>.js</code> file.</p>
<h2>Option 2: import interfaces from a .d.ts file</h2>
<p>The other option to define interfaces is to put them in a <code>.d.ts</code> file and import that into you <code>.js</code> file.</p>
<pre class="language-ts"><code class="language-ts"><span class="token comment">// models.d.ts</span>
<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">Address</span> <span class="token punctuation">{</span>
street<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
city<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
zip<span class="token operator">:</span> <span class="token builtin">number</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">export</span> <span class="token keyword">interface</span> <span class="token class-name">Customer</span> <span class="token punctuation">{</span>
name<span class="token operator">:</span> sting<span class="token punctuation">;</span>
email<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span>
address<span class="token operator">:</span> Address<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<p>And then in your <code>.js</code> file you can import those types:</p>
<pre class="language-js"><code class="language-js"><span class="token comment">/**
* @typedef { import("./models").Customer } Customer
*/</span>
<span class="token comment">/** @type {Customer} */</span>
<span class="token keyword">const</span> theCustomer <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span> <span class="token punctuation">}</span></code></pre>
<p><img src="https://goulet.dev/posts/how-to-write-ts-interfaces-in-jsdoc/defined-in-dts.jpg" alt="Importing from .d.ts file" /></p>
<p>This is my preferred approach as I find writing types in <code>.d.ts</code> files to be more terse than writing types out in jsdoc comments. But I like having both options available to use interchangeably.</p>
<h3>Related Posts</h3>
<ul>
<li><a href="https://goulet.dev/posts/type-guard-in-jsdoc/">Type Guards in Javascript Using JSDoc Comments</a></li>
</ul>
Scoped CSS Styles with Declarative Shadow DOM
Need to scope some CSS to a specific element in an SSR-friendly way? You can do it with web standards, no framework needed!
2023-07-27T00:00:00Z
https://goulet.dev/posts/scoped-styles-dsd/
<p><img src="https://goulet.dev/posts/scoped-styles-dsd/codepen.png" alt="Codepen showing Declarative Shadow DOM" /></p>
<p>I've always thought of the Shadow DOM as something to use for custom elements only... but it can be useful for non-custom elements as well!</p>
<p>I was reading <a href="https://medium.com/@eisenbergeffect/a-few-dom-reminders-2a0f18e40804">this excellent post</a> by Rob Eisenberg and came across the part about composing shadow DOM and realized that you can use <a href="https://developer.chrome.com/articles/declarative-shadow-dom/">Declarative Shadow DOM</a> to scope CSS to a particular element without any JS. And without any framework/dependencies. πͺ</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>template</span> <span class="token attr-name">shadowrootmode</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>open<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>style</span><span class="token punctuation">></span></span><span class="token style"><span class="token language-css">
<span class="token selector">:host</span> <span class="token punctuation">{</span>
<span class="token property">color</span><span class="token punctuation">:</span> red<span class="token punctuation">;</span>
<span class="token property">font-size</span><span class="token punctuation">:</span> 1.5rem<span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>style</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>slot</span><span class="token punctuation">></span></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>slot</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>template</span><span class="token punctuation">></span></span>
This paragraph is scoped
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span></code></pre>
<p>You can see a live example at <a href="https://codepen.io/wes_goulet/pen/wvQEJod">this Codepen</a>.</p>
<blockquote>
<p>One thing to note is that as of July 2023 Firefox has not implemented Declarative Shadow DOM yet (see <a href="https://caniuse.com/mdn-html_elements_template_shadowrootmode">caniuse</a>), but a <a href="https://developer.chrome.com/articles/declarative-shadow-dom/#polyfill">polyfill</a> is possible. Firefox <a href="https://caniuse.com/mdn-api_element_attachshadow">does currently support</a> the imperative <code>attachShadow</code> API.</p>
</blockquote>
<h2>Imperative Shadow DOM</h2>
<p>Alternately, you can use the imperative <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow"><code>attachShadow</code> JS API</a> to accomplish the same thing. It is supported in all modern browsers, however it's more verbose, requires JS, and is not as SSR-friendly as Declarative Shadow DOM. For an example of using <code>attachShadow</code> check out <a href="https://codepen.io/wes_goulet/pen/NWELmoy">this codepen</a>.</p>
<p>The imperative Shadow DOM works well when writing <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM">custom elements</a> (like <a href="https://github.com/wes-goulet/side-drawer">side-drawer</a>) and declarative Shadow DOM works well when styling a non-custom element (like <code><p></code> or <code><button></code>).</p>
Don't use TypeScript enum
Enums are not part of the JavaScript language, use string unions instead.
2023-12-09T00:00:00Z
https://goulet.dev/posts/no-typescript-enum/
<blockquote>
<p>Trying my hand at a <a href="https://chriscoyier.net/2023/10/02/dan-mall-answers-to-common-design-questions/">zero-nuance take</a>.</p>
</blockquote>
<h2>Don't do this</h2>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">enum</span> Shape <span class="token punctuation">{</span>
Sphere<span class="token punctuation">,</span>
Rectangle<span class="token punctuation">,</span>
Triangle<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token keyword">const</span> ball <span class="token operator">=</span> <span class="token punctuation">{</span>
shape<span class="token operator">:</span> Shape<span class="token punctuation">.</span>Sphere<span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h2>Do this instead</h2>
<pre class="language-ts"><code class="language-ts"><span class="token keyword">type</span> <span class="token class-name">Shape</span> <span class="token operator">=</span> <span class="token string">"Sphere"</span> <span class="token operator">|</span> <span class="token string">"Rectangle"</span> <span class="token operator">|</span> <span class="token string">"Triangle"</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> ball <span class="token operator">=</span> <span class="token punctuation">{</span>
shape<span class="token operator">:</span> <span class="token string">"Sphere"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span></code></pre>
<h2>Why?</h2>
<blockquote>
<p>Seems I'm not good at "zero-nuance", I feel compelled to explain the why behind this take... at least I'll keep it brief.</p>
</blockquote>
<p>Enums are not part of the JavaScript language, so the TypeScript compiler generates different runtime code when you use enums. You aren't writing "JavaScript with types" any more. You are writing a different language that transpiles to JS. Don't do that. Keep it simple.</p>
<p>TypeScript is great for type checking as a dev-time benefit: it's additive, you can strip off the types and you still have JavaScript. You can even add types to JavaScript files with JSDoc comments - no need for a build step to strip off types, ship/debug the code you write, even better!</p>
<h3>Related Posts</h3>
<ul>
<li><a href="https://goulet.dev/posts/how-to-write-ts-interfaces-in-jsdoc/">How to Write TypeScript Interfaces in JSDoc Comments</a></li>
<li><a href="https://goulet.dev/posts/type-guard-in-jsdoc/">Type Guards in Javascript Using JSDoc Comments</a></li>
</ul>
Conditional Styles with CSS `:has()`
A simple example of conditional styling with :has and :checked, no JS needed.
2024-03-10T00:00:00Z
https://goulet.dev/posts/conditional-styles-with-has/
<p>I was setting up a simple landing page for <a href="https://fandwagon.com/">Fandwagon</a> and just wanted some text to be strike-through when the user checks a checkbox. So far my landing page has no need for any JavaScript, and thankfully I can keep it that way and still get conditional styles with the <code>:has()</code> pseudo-class.</p>
<h2>Demo</h2>
<style>
#example-form {
padding: 1.5rem;
border-radius: 1rem;
background-color: color-mix(in srgb, black 10%, transparent);
&:has(input:checked) span {
text-decoration: line-through;
}
& > p {
margin-top: 0;
}
& > div {
display: flex;
align-items: center;
}
}
</style>
<form id="example-form">
<p>
Want to know when it's ready?
<span>I'll send you one email when it's ready, then delete your email address
forever.</span>
</p>
<div>
<input type="checkbox" switch="" name="subscribe" id="subscribe" />
<label for="subscribe">Check this box to subscribe to updates</label>
</div>
</form>
<blockquote>
<p>βοΈ Bonus: inspect the demo above βοΈ in your browser dev tools to see how to use <code>:has</code> in nested CSS, and also how to use <code>color-mix</code> to create a semi-transparent background color.</p>
</blockquote>
<h2>Code</h2>
<p>First, the HTML - a simple form with a checkbox.</p>
<pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation"><</span>form</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>p</span><span class="token punctuation">></span></span>
Want to know when it's ready?
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>span</span>
<span class="token punctuation">></span></span>I'll send you one email when it's ready, then delete your email address
forever.<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>span</span>
<span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>p</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>input</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checkbox<span class="token punctuation">"</span></span> <span class="token attr-name">switch</span> <span class="token attr-name">name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>subscribe<span class="token punctuation">"</span></span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>subscribe<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>label</span> <span class="token attr-name">for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>subscribe<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>Check this box to subscribe to updates<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>label</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"></</span>form</span><span class="token punctuation">></span></span></code></pre>
<p>And the CSS for the conditional strike-through</p>
<pre class="language-css"><code class="language-css"><span class="token selector">#example-form:has(input:checked) span</span> <span class="token punctuation">{</span>
<span class="token property">text-decoration</span><span class="token punctuation">:</span> line-through<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre>
<h2>Breaking down the selector</h2>
<p>The first part <code>#example-form:has(input:checked)</code> is basically saying "find the element with id of <code>example-form</code> that <em>has</em> a child <code>input</code> element that is checked".</p>
<p><code>:has</code> is basically a very-flexible "parent selector".</p>
<p>Once we've selected the parent that contains the checked <code>input</code> then we select the descendent <code>span</code> element and apply the <code>text-decoration: line-through</code> style.</p>
<p>π₯ Done! No JavaScript needed!</p>
<blockquote>
<p>You can get pretty far in conditional styling by combining <code>:has</code> and <code>:checked</code> pseudo-classes. If you have any cool example of combining the two then send me an email, I'd love to see!</p>
</blockquote>
2024-03-28T18:00:33Z
https://goulet.dev/posts/
<header>
<nav>
<a class="logo-link" href="https://goulet.dev/"><img class="logo" alt="goulet.dev logo" src="https://goulet.dev/static/logo.svg" /></a>
<div class="nav-buttons">
<a class="" href="https://goulet.dev/">Home</a>
<a class="active" href="https://goulet.dev/posts">Posts</a>
<a class="" href="https://goulet.dev/projects">Projects</a>
</div>
</nav>
</header>
<main>
<section class="hero row">
<h1>All blog posts</h1>
<ul class="post-list">
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/conditional-styles-with-has/">Conditional Styles with CSS `:has()`</a></div>
<div>
<p>Sun Mar 10 2024</p>
<div>
<div class="badge">css</div>
<div class="badge">tip</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/no-typescript-enum/">Don't use TypeScript enum</a></div>
<div>
<p>Sat Dec 09 2023</p>
<div>
<div class="badge">javascript</div>
<div class="badge">typescript</div>
<div class="badge">enums</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/scoped-styles-dsd/">Scoped CSS Styles with Declarative Shadow DOM</a></div>
<div>
<p>Thu Jul 27 2023</p>
<div>
<div class="badge">javascript</div>
<div class="badge">dom</div>
<div class="badge">css</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/how-to-write-ts-interfaces-in-jsdoc/">How to Write TypeScript Interfaces in JSDoc Comments</a></div>
<div>
<p>Sun Jul 23 2023</p>
<div>
<div class="badge">typescript</div>
<div class="badge">javascript</div>
<div class="badge">jsdoc</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/theme-color-meta-tag/">theme-color meta tag vs manifest theme_color</a></div>
<div>
<p>Sun Jun 25 2023</p>
<div>
<div class="badge">pwa</div>
<div class="badge">web</div>
<div class="badge">design</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/app-for-that/">There is an App for That (But Shouldn't Be)</a></div>
<div>
<p>Sun Jun 04 2023</p>
<div>
<div class="badge">app store</div>
<div class="badge">apple</div>
<div class="badge">ios</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/error-serialization-in-js/">How to Serialize Errors in Javascript</a></div>
<div>
<p>Sun Apr 30 2023</p>
<div>
<div class="badge">javascript</div>
<div class="badge">errors</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/supabase-webhook-local/">Running Database Webhooks Locally with Supabase CLI</a></div>
<div>
<p>Sat Mar 11 2023</p>
<div>
<div class="badge">docker</div>
<div class="badge">backend</div>
<div class="badge">supabase</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/netlify-functions-js-typing/">Netlify Functions in Javascript with Type-Checking</a></div>
<div>
<p>Sat Mar 11 2023</p>
<div>
<div class="badge">jsdoc</div>
<div class="badge">javascript</div>
<div class="badge">netlify</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/emoji-shortcodes-in-eleventy/">Enabling Emoji Shortcodes in Eleventy Markdown Files</a></div>
<div>
<p>Wed Nov 24 2021</p>
<div>
<div class="badge">11ty</div>
<div class="badge">eleventy</div>
<div class="badge">emoji</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/resume-highlights/">Make Your Resume Stand Out with Highlights</a></div>
<div>
<p>Sat Apr 17 2021</p>
<div>
<div class="badge">career</div>
<div class="badge">opinion</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/getting-started-as-a-dev/">Getting Started as a Software Developer</a></div>
<div>
<p>Sun Mar 07 2021</p>
<div>
<div class="badge">development</div>
<div class="badge">opinion</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/how-to-install-arm-node-on-apple-silicon/">How To Install Node on macOS with Apple Silicon</a></div>
<div>
<p>Thu Dec 10 2020</p>
<div>
<div class="badge">node</div>
<div class="badge">arm</div>
<div class="badge">m1</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/detect-pwa-visibility/">Detect if a PWA is Visible with the Page Visibility API</a></div>
<div>
<p>Sat Nov 07 2020</p>
<div>
<div class="badge">pwa</div>
<div class="badge">standards</div>
<div class="badge">api</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/type-guard-in-jsdoc/">Type Guards in Javascript Using JSDoc Comments</a></div>
<div>
<p>Sat Oct 10 2020</p>
<div>
<div class="badge">javascript</div>
<div class="badge">typescript</div>
<div class="badge">jsdoc</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/ios14-pwa-improvements/">iOS 14 PWA Improvements</a></div>
<div>
<p>Sun Jul 12 2020</p>
<div>
<div class="badge">web</div>
<div class="badge">pwa</div>
<div class="badge">ios</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/why-have-a-dev-blog/">Four Reasons For Having a Blog</a></div>
<div>
<p>Sun Jul 05 2020</p>
<div>
<div class="badge">blogging</div>
<div class="badge">writing</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/zsh-guide/">A Simple Guide to Setting Up zsh</a></div>
<div>
<p>Sat Jan 04 2020</p>
<div>
<div class="badge">bash</div>
<div class="badge">zsh</div>
<div class="badge">shell</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/how-to-password-protect-staging-on-netlify/">How To Password Protect Your Staging Site on Netlify</a></div>
<div>
<p>Sat Nov 30 2019</p>
<div>
<div class="badge">netlify</div>
<div class="badge">devops</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/how-to-set-windows-terminal-starting-directory/">How To Set Windows Terminal Starting Directory for WSL</a></div>
<div>
<p>Sun Nov 17 2019</p>
<div>
<div class="badge">windows</div>
<div class="badge">terminal</div>
<div class="badge">wsl</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/build-a-pwa-without-a-build-step/">How to Build a PWA without a Build Step</a></div>
<div>
<p>Sun Nov 10 2019</p>
<div>
<div class="badge">lit-element</div>
<div class="badge">rollup</div>
<div class="badge">buildless</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/how-to-make-a-pwa/">How to Build a PWA</a></div>
<div>
<p>Sat Aug 31 2019</p>
<div>
<div class="badge">web</div>
<div class="badge">pwa</div>
<div class="badge">ios</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/ios13-pwa-improvements/">iOS 13 PWA Improvements</a></div>
<div>
<p>Sun Aug 11 2019</p>
<div>
<div class="badge">web</div>
<div class="badge">pwa</div>
<div class="badge">ios</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/quotes-around-globs/">Glob Pattern Not Working? Don't Forget the 'Quotes'</a></div>
<div>
<p>Sat Mar 30 2019</p>
<div>
<div class="badge">prettier</div>
<div class="badge">glob</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/consuming-web-component-react-typescript/">Consuming a Web Component in React in Typescript</a></div>
<div>
<p>Sat Dec 01 2018</p>
<div>
<div class="badge">react</div>
<div class="badge">typescript</div>
<div class="badge">web components</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/fix-css-tap-effects-on-ios/">Fixing CSS Tap Effects on iOS</a></div>
<div>
<p>Sun Nov 18 2018</p>
<div>
<div class="badge">safari</div>
<div class="badge">css</div>
<div class="badge">ios</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/react-handle-keyboard-shortcuts/">Handling Keyboard Shortcuts in React</a></div>
<div>
<p>Sat Sep 08 2018</p>
<div>
<div class="badge">react</div>
<div class="badge">keyboard shortcuts</div>
<div class="badge">a11y</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/node-dyld-library-not-loaded-icu4c/">Fixing Node Library Not Loaded icu4c Error</a></div>
<div>
<p>Sat Sep 01 2018</p>
<div>
<div class="badge">node</div>
<div class="badge">yarn</div>
<div class="badge">brew</div>
</div>
</div>
</li>
<li class="post-list-item">
<div><a href="https://goulet.dev/posts/avoiding-git-problems-when-installing-a-theme-to-hugo/">Avoiding Git Problems When Installing a Theme to Hugo</a></div>
<div>
<p>Sat Aug 25 2018</p>
<div>
<div class="badge">hugo</div>
<div class="badge">theme</div>
<div class="badge">git</div>
</div>
</div>
</li>
</ul></section>
</main>
<footer>
<div class="row">
<div>
<p>Β©2024 - goulet.dev</p>
</div>
<div>
<p>
<a href="https://goulet.dev/">Home</a> | <a href="https://goulet.dev/posts">Posts</a> |
<a href="https://goulet.dev/projects">Projects</a>
</p>
<p>
<a rel="me" href="https://twitter.com/wes_goulet" target="_blank">Twitter</a>
|
<a rel="me" href="https://techhub.social/@goulet" target="_blank">Mastodon</a>
</p>
<p>
<a rel="me" href="https://github.com/wes-goulet" target="_blank">GitHub</a>
</p>
</div>
</div>
</footer>