Skip to main content
Nick Taylor
AI Engineer
View all authors

Supercharge Your GitHub Profile: Showcase Your Open Source Contributions

· 4 min read
Nick Taylor
AI Engineer

Last week, we launched our brand new Dev Cards with the OSCR (Open Source Contributor Rating) highlighted on the front to make it easier for you to showcase your impact in open source. To make it even easier, I've cooked up a little GitHub Action that'll automatically update your Dev Card on your GitHub profile.

The React useRef Hook: Not Just for DOM Elements

· 4 min read
Nick Taylor
AI Engineer

In this post, we'll cover what the useRef hook is, some examples of how it can be used, and when it shouldn't be used.

What is useRef?

The useRef hook creates a reference object that holds a mutable value, stored in its current property. This value can be anything from a DOM element to a plain object. Unlike component state via say the useState hook, changes to a reference object via useRef won't trigger a re-render of your component, improving performance.

Creating an OG image using React and Netlify Edge Functions

· 4 min read
Nick Taylor
AI Engineer

Open Graph (OG) images are a must if you're sharing content on the Internet. From sites like X/Twitter, to Slack or Discord, a great OG image makes your link share pop.

Examples

I recently built out a couple of OG images for Open Sauced for a couple of features we've rolled out over the past couple of months, Workspaces and Repository pages. They're great features that I encourage you to check out, and I encourage you to share them on socials so our beautiful OG images pop.

For example, here's an OG image for a workspace for jsr. JSR is the new JavaScript registry from the folks from Deno.

OG image for the OpenSauced workspace for jsr

And here's the OG image for a repository page for huggingface/transformers.

OG image for the huggingface/transformers repository

Looking at the image for the jsr workspace, there is a template for the image, but there are several dynamic portions to the image.

All the sections denoted by green outlined squares are dynamic.

OG image for the OpenSauced workspace for jsr with sections outline in green squares denoting dynamic portions of the image

This dynamic info gets pulled in for the most part from the OpenSauced API.

Other parts are pulled in from the URL, like 30 for the day range, and the description comes from the query string in the OG image URL.

browser dev tools view of the metadata section of the head with OG image URLs outlined by green squares

React to generate an image

So, how do we use React to generate an image?

We're using og_edge from my old co-worker Matt Kane (@ascorbic), but og_edge is a direct port of @vercel/og that works on Deno and Netlify Edge Functions which run on Deno.

https://github.com/ascorbic/og-edge

Under the hood, og_edge and @vercel/og use the Satori library.

Satori: Enlightened library to convert HTML and CSS to SVG.

https://github.com/vercel/satori

The API for the og_edge module is pretty straightforward. It exposes an ImageResponse constructor with the following options and that's it.

{% raw %}
new ImageResponse(
element: ReactElement,
options: {
width?: number = 1200
height?: number = 630
emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' | 'fluent' | 'fluentFlat' = 'twemoji',
fonts?: {
name: string,
data: ArrayBuffer,
weight: number,
style: 'normal' | 'italic'
}[]
debug?: boolean = false

// Options that will be passed to the HTTP response
status?: number = 200
statusText?: string
headers?: Record<string, string>
},
)
{% endraw %}

Code snippet above care of the official og_edge API reference.

To build out these OG images, we have a background image, some icons, like a star and fork icon, and we also pull in the repository organization or user's avatar. With a bit of vanilla CSS, we can position things just right. We also pull in the Inter font as that's what we use at OpenSauced.

As far as I know, og_edge does not support Tailwind like @vercel/og does. Not a dealbreaker at all, but just something to be mindful of.

One other thing we do is set cache headers as these are dynamic images where the data changes over time. Having said that, some social networks cache OG images very aggressively.

{% raw %}
headers: {
// cache for 2 hours
"cache-control": "public, s-maxage=7200",
"content-type": "image/png",
},
{% endraw %}

Show me the code

Here's the pull requests for the initial work on these two OG images.

https://github.com/open-sauced/app/pull/2939 https://github.com/open-sauced/app/pull/3117

Wrapping up

Beautiful and dynamic OG images are a must if you're looking to stand out when sharing links on socials, and og_edge and @vercel/og are great options if you also want to leverage your existing React skill set.

Now go out and build your own OG images! 🖼️

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Form and Function: How I Lost My Submit Button & Got It Back

· 4 min read
Nick Taylor
AI Engineer

As web developers, we know that most of the create, read update, and delete (CRUD) actions we perform on the web are typically (hopefully?) done using an HTML form.

HTML Forms

HTML Forms are cool because they have plenty of built-in features.

For example, they have native form validation and access to all the inputs in a form, and at some point, because you need to submit the form, there is a mechanism to do that as well. You can use a button, <button>submit</button> or an input of type submit, <input type="submit" />, although the latter isn't used as much these days in new sites, from what I've seen.

Here is a simple form to exhibit this.

https://codepen.io/nickytonline/pen/JjVOarX

If you fill out the form and click submit, the form will submit and add a paragraph with dark green text client-side that says, "Form submitted".

Submitting the simple form

There are other things in the simple form, like form validation via the required attribute on inputs, but that's not what we're here to discuss.

What we want to touch on is that the form was able to handle the submit event because it had a submit button associated with it, which was defined in HTML within the form element.

Note: you can press enter in fields to submit a form, but again, not what we're here to discuss.

How I Broke My Form

This brings us to a new feature that I was working on for OpenSauced for a few months, workspaces. I encourage you to create your own, but for now, let's get back to the business of forms.

Here's our beautiful workspaces settings page that I implemented.

an OpenSauced workspace settings page

Recently, there were styling changes, which is what you see above.

https://github.com/open-sauced/app/pull/2982

Everything looked great, and I had tested it.

Narrator: he thought he had tested it, and we shipped things to production.

Once things went live, I decided to do some smoke tests, which I usually do. I went over to the beautiful workspace settings I had worked on, made some changes in the settings, and then clicked Update Workspace button. Hmm, no toast message saying the settings were updated. I checked the browser dev tools to see if there were any JavaScript errors. Nothing related to the updates. And then it dawned on me. The submit button was outside the form, and I just broke some key functionality in the app.

Michael Scott telling everybody not to panic.

Side note, but luckily thanks to Netlify's deployment rollback feature, I was able to revert to the previous production deployment within about a minute of the workspace settings page being broken 😅

How I Fixed My Form

So how did I fix it? We needed this new styling to fix several other issues related to z-indexes and layout.

For some context, the OpenSauced application is a Next.js site, so React, but I decided to put on my old school HTML hat and remembered that form elements can be associated to a form via a form attribute. What you need to do is give the form an id attribute, and the form element that you want to associate the form to needs to have a form attribute whose value is the value of the id attribute for the form.

Here's another simple form demonstrating a simplified version of my fix.

https://codepen.io/nickytonline/pen/XWQzPOX

I encourage you to remove the form attribute from the button in the above CodePen to see the issue I caused.

Here's the fix I rolled out to production.

https://github.com/open-sauced/app/pull/3003

Wrapping Up

Learning a framework is great, and I'm a big proponent of just building something, but as you continue on in your career, it's great to start getting some fundamentals into the mix.

Also, this is a perfect example of why using semantic HTML is important! It definitely helped me get out of jam! 😅

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Stuck in the Middle with You: An intro to Middleware

· 8 min read
Nick Taylor
AI Engineer

Middleware exists in several frameworks like Next.js, Express, Hono and Fresh, and not just in JavaScript land. You can find it in frameworks like ASP.NET core in the .NET ecosystem, or Laravel in PHP. Since I mainly work in JavaScript and TypeScript these days, examples will be from frameworks in those ecosystems.

Middleware is something that happens in the middle of a user or some service interacting with a site or API call and happens at the framework level.

It runs before a page loads or an API endpoint is called, or more generally a route. There are many reasons why you might want to do that:

  • gate certain content, e.g. a private route
  • set and read cookies
  • add headers to the response being sent out
  • URL redirect, e.g. redirecting to another page based on some criteria
  • URL rewrites

Let's dig in!

Gate Content

Authentication and authorization are two great candidates for guarding certain routes, although it’s still good to guard access to privied resources in API endpoints and pages. In this context, think of the middleware as the first line of defense.

Gandfalf saying, "You shall not pass!"

In the OpenSauced application, when a user logs in and the path is /workspaces we redirect them to their workspace.

{% raw %}
if (session?.user && req.nextUrl.pathname === "/workspaces") {
const data = await loadSession(req, session?.access_token);
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);

return NextResponse.redirect(`${workspaceUrl}`);
}
{% endraw %}

Code on GitHub

Setting and Reading cookies

So what is a cookie?

A cookie is a way to set a piece of user-specific data. This could be a session ID for someone who is logged in to a site, or it could be some other user data. Note that the data in a cookie is typically not that large, but according to MDN, there is no size limit to the name or value of a cookie.

Cookie monster eating cookies

Cookies that are HTTP only can be accessed on the server-side, but for cookies that are not HTTP only, they can be accessed server-side and client-side. For example, you wouldn't want someone to tamper with your session ID on the client-side, so this type of cookie is set as HTTP only.

We recently shipped a new feature at OpenSauced, called Workspaces. You can read all about it in this great post from my co-worker Bekah (@BekahHW).

https://dev.to/opensauced/navigating-the-challenges-of-scaling-open-source-projects-11h2

TLDR; a Workspace acts like a workspace in other software you may have used, like Notion. One thing required for this feature is when a user navigates to the /workspaces URL, it has to load the last accessed workspace. If a user has never accessed a workspace before, it should default to their personal workspace. This is a perfect use case to leverage using a cookie.

When someone logs in, we check if they have a workspace ID cookie set. If they don’t, we grab their personal workspace ID, a type of workspace every user has.

The code for this was in the code snippet in the last section.

{% raw %}
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);
{% endraw %}

Let's take a peek into the getWorkspaceUrl function.

{% raw %}
export function getWorkspaceUrl(cookies: RequestCookies, baseUrl: string, personalWorkspaceId: string) {
if (!cookies.has(WORKSPACE_ID_COOKIE_NAME)) {
cookies.set(WORKSPACE_ID_COOKIE_NAME, personalWorkspaceId);
}

// @ts-expect-error the cookie value will be defined
const workspaceId = cookies.get(WORKSPACE_ID_COOKIE_NAME).value;

return new URL(`/workspaces/${workspaceId}`, baseUrl);
}
{% endraw %}

If there is no workspace cookie set, we create a cookie and set its value to the user's personal workspace ID.

After that, we read the cookie, we build a URL with it and the user is redirected to the workspace.

The other piece of this that doesn't occur in middleware is when a user visits a valid workspace page they have access to, we set the workspace ID cookie. Next time they go to the /workspaces link, the cookie will exist, and a URL using new URL() will be used to redirect them to the last accessed workspace homepage.

The page will call the OpenSauced app's setCookie function.

{% raw %}
export function setCookie({
response,
name,
value,
maxAge = 31536000,
sameSite = "Lax",
}: {
response: Response;
name: string;
value: string;
maxAge?: number;
sameSite?: SameSite;
}) {
response.setHeader(
"Set-Cookie",
`${name}=${value}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=${sameSite}; Secure`
);
}
{% endraw %}

Code on GitHub

Although cookies are their own thing, you have set them in a header.

Add Headers

As mentioned in the previous section, you set cookies via a header. So what is a header, more specifically, an HTTP header?

Headers are a set of key value pairs to let a browser know how to behave, for example, should a page be cached? It can also be custom key value pairs that your application or services might need. For example, when I worked at Netlify, for the CDN to work, there would be Netlify-specific headers that once inside the internal network would allow Netlify to do some magic.

If you go to my website, nickyt.co, and open the network panel in the dev tools of your browser of choice, you'll see some Netlify-specific headers.

Response headers from nickyt.co's homepage showing some custom Netlify headers being set

I recently gave a talk on Fresh, a full-stack framework for Deno at Node Summit '24. The recording isn't up yet, but here's the slide deck and code from the demo for anyone interested.

In Fresh middleware, this is how you could set a header.

{% raw %}
export async function handler(
request: Request,
ctx: FreshContext<State>
) {
const response = await ctx.next();
response.headers.set("x-fresh", "true");

if (request.url.includes("joke-of-the-day")) {
response.headers.set("x-joke-page", "true");
}

if (request.url.includes("movie/")) {
response.headers.set("x-movie-page", "true");
}

return response;
}
{% endraw %}

Code on GitHub

In the above code snippet, we're checking to see if a specific route contains a certain string and if it does, we set a custom header, e.g.

{% raw %}
response.headers.set("x-joke-page", "true");
{% endraw %}

URL Redirection

Page redirection allows you to have a URL go to another URL. You might do this for a couple of reasons. Maybe a bunch of links on your site changed, and you need to have them go to a new set of links, or you have a URL that needs to redirect to a user-specific page.

Kermit the frog looking at a map trying to figure out where to go

For non-trivial redirects like the workspaces redirect URL mentioned in one of the previous sections, middleware is a great place for handing redirects.

{% raw %}
if (session?.user && req.nextUrl.pathname === "/workspaces") {
const data = await loadSession(req, session?.access_token);
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);

return NextResponse.redirect(`${workspaceUrl}`);
}
{% endraw %}

Code on GitHub

In this case, when someone in the OpenSauced application goes to /workspaces we redirect them to a user-specific URL.

{% raw %}
return NextResponse.redirect(`${workspaceUrl}`);
{% endraw %}

Not a hard and fast rule, but if you have trivial redirects like redirect /old-blog-path/* to /blog/*, consider using your hosting platform's redirects instead of middleware.

URL Rewriting

You can also do URL rewrites. It's like a redirect, but the URL never changes. Frameworks like Next.js provide this out of the box in their configuration file, but for more complex handling, you may want to do it in middleware. So what is a URL rewrite? A rewrite will preserve the existing URL but will render content from another URL.

Mr. Burns from the Simpsons saying, "Well, cover it with a rewrite"

Here's a slightly modified example straight out of the Next.js middleware documentation:

{% raw %}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
{% endraw %}

Code in Next.js documenation

In the above snippet, all users have a /dashboard page they go to, but every user's dashboard is different. In this case, the user will always see the page as /dashboard but it loads the specific user's dashboard.

Resources

Here's the documentation for middleware of the mentioned frameworks:

Wrapping Up

Middleware is a great tool and if your framework of choice supports middleware (most do), I encourage you to read up on how to leverage it in that framework.

What use cases have you used middleware for? Please let me know in the comments.

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Challenging the Skeptics: Unveiling the Undeniable Goodness of Tailwind CSS

· 6 min read
Nick Taylor
AI Engineer

People definitely have opinions about Tailwind. There are staunch supporters and staunch haters, but I really don't want to get into all that. Head on over to Twitter if you want to waste some time.

If you're pretty well versed with Tailwind, this article might not be for you, but who knows? Read on and maybe you'll learn something.

I'm coming in with what, I think, is a fresh perspective. I'm using Tailwind for the first time professionally. Furthermore, I don't consider myself a CSS expert, but I think I have pretty solid CSS skills.

I mention all this, to convey a sentiment, I've seen many people exhibit. You're using Tailwind because you don't understand CSS. I do understand CSS.

So the first thing that I've seen when people say when they do not like Tailwind, is that it's not CSS, or it's inline CSS. This is completely false, even coming in as a newbie to Tailwind, all Tailwind is, at the end of the day, once it's compiled, is CSS utility classes.

Comparisons

So let's look at some comparisons between Tailwind and "real" CSS. I'm going to put the vanilla CSS in a style tag, but you could also put it in a .css file and link it in the head of your HTML or however your application bundles CSS. This is just for the sake of comparison.

First Glances of Tailwind

Vanilla CSS

{% raw %}
<style>
.my-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}

.my-list li {
border-width: 1px;
}
</style>
<ul class="my-list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
{% endraw %}

Tailwind

{% raw %}
<ul class="flex flex-col gap-6">
<li class="border">Item 1</li>
<li class="border">Item 2</li>
<li class="border">Item 3</li>
</ul>
{% endraw %}

So the first thing someone might say is that Tailwind is repeating the border CSS class on a list item, <li>, instead of using a selector that can target the li DOM elements. This is true, but Tailwind allows you to create the equivalent of .my-list li. You can do the following:

{% raw %}
<ul class="flex flex-col gap-6 [&_li]:border">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
{% endraw %}

This is probably where someone might say, "Well, now you're just writing inline CSS." This is also false. It will generate a CSS rule based on the [&_li]:border CSS class name. It will compile it to literal CSS that will generate an equivalent CSS rule comparable to the CSS rule for the .mylist li selector.

In fact, this is what it compiles to. I've formatted it since it gets minified.

{% raw %}
.\[\&_li\]\:border li {
border-width: 1px;
}
{% endraw %}

You could make an argument that the "real" version looks nicer, but this isn't a strong argument, and you have CSS source maps if you open the browser dev tools.

I'll say it here and repeat it again later. Tailwind is a utility-first CSS framework. It's not inline CSS.

If you want to see an example of this in production grade code, check out a recent pull request (PR) of mine to the OpenSauced app repository.

https://github.com/open-sauced/app/pull/2524

Styling pseudo-elements

What about something more complex like pseudo-elements? Let's take the ::before pseudo-element for a spin.

Vanilla CSS

{% raw %}
<style>
.pizza-time::before {
content: attr(data-inset-label);
}
</style>
<p data-inset-label="🍕" class="pizza-time">OpenSauced is awesome!</p>
{% endraw %}

Tailwind

{% raw %}
<p data-inset-label="🍕" class="before:content-[attr(data-inset-label)]">
OpenSauced is awesome!
</p>
{% endraw %}

Here's what it generates as CSS when Tailwind compiles that CSS class.

{% raw %}
.before\:content-\[attr\(data-inset-label\)\]:before{
--tw-content:attr(data-inset-label);
content:var(--tw-content)
}
{% endraw %}

You could complain that that is one hell of a bloated CSS class name, but again, I don't think this is a colossal deal.

If you want to see an example of this in production grade code, check out a recent PR of mine to the OpenSauced app repository.

https://github.com/open-sauced/app/pull/2552

Animations

If you're looking to add animations, Tailwind ships with plenty of useful animations and CSS classes to leverage them.

Need a custom animation? You can do that as well. I won't go into it here, but here's a great post about writing custom animations in Tailwind.

Accessibility

You've got all these cool animations, but what if someone has specified prefers-reduced-motion? Tailwind can handle that for you as long as you prefix your animation with motion-safe:, e.g.

{% raw %}
<p class="motion-safe:animate-spin">Spinning text</p>
{% endraw %}

There's other useful Tailwind classes for accessibility, like sr-only, which will remain in the page, but only be visible to screen readers.

I think something that would be interesting to add to the Tailwind a11y story is using Tatiana Mac's (@tatianamac) approach of taking a no-motion-first approach to animations.

Define some base styles

I'm all for components, and I'm a big fan of JSX. Tailwind pairs nicely with components, but I do think that it's still good to have some base styles defined, even if you are using components.

For example, a base font size and colour, focus state styles, headings etc. This is what I ended up doing in the OpenSauced app repository.

Another Complaint: It's like bootstrap

Tailwind CSS on its own is not like bootstrap. It's just CSS utility classes, whereas bootstrap is UI components and CSS.

I've never used it, but maybe you could fall into this trap with Tailwind UI.

Tradeoffs

Like many things, there are tradeoffs. I think the biggest one is learning the Tailwind CSS classes and naming conventions for building them, but I think the benefits outweigh this. And to be honest, once you start writing the classes frequently, the naming convention just sticks in your head.

And if you have some super complex CSS, for whatever reason, Tailwind can't handle, there's nothing wrong with adding some custom CSS.

Wrapping Things Up

I literally only started using Tailwind September 18th of 2023 when I started at OpenSauced.

Tailwind has made me super productive while building out OpenSauced, and I've used it in some other projects since then.

Remember, Tailwind is a utility-first CSS framework. It's not inline CSS.

I encourage you to give Tailwind a go. They have outstanding documentation and great IDE support to help you along the way.

If you give it a go and say it's not for me, that's OK. Use what makes you the most productive.

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Unlocking the Power of HTML's Native Browser Dialog Element

· 4 min read
Nick Taylor
AI Engineer

All the major browsers now support the &lt;dialog%gt; element. Why add this HTML element? User land code, code that developers write to fill in gaps of the browser, was doing similar things repeatedly, especially around focus trapping, and browser engines responded by adding this functionality directly in the browser.

Focus Trapping

What is focus trapping? It's a feature where you do not want focus outside a specific element, and that element typically contains focusable elements.

For example, a form in a modal to confirm an action: As a user uses the keyboard to navigate, they go to the next focusable element, e.g. a button.

If they reach the last focusable element in the modal, without focus trapping, the focus would go to the next focusable element in the document object model (DOM). With focus trapping, you go from the last focusable back to the first focusable element in the parent element.

In user land, popular packages like focus-trap have enabled developers to incorporate focus trapping.

<dialog> for Modal Dialogs

With the dialog element, you get this for free, although there is a gotcha. If you add a dialog element to the page with the open attribute set, the dialog element will become visible on the page; however, focus trapping will not work as you'd expect in a modal.

From the API documentation:

Note: While you can toggle between the open and closed states of non-modal dialog boxes by toggling the presence of the open attribute, this approach is not recommended.

To get focus trapping working, the JavaScript API is required. You can display a modal on the screen by calling the HTMLDialogElement showModal method.

Note that you'll need to view this CodePen in full view because, for some reason, modal dialog focus trapping does not work in the CodePen editor view.

https://codepen.io/nickytonline/pen/NWJvbPe

Not only do you get focus trapping, you also get modal close functionality that people have come to expect via the Escape key.

All of that is already amazing, but another common thing people were doing in user land was adding a background to block out users from interacting with the page. With the &lt;dialog%gt; element, we can add a ::backdrop pseudo-element that does this for you. All you need to do is style it. In the CodePen above, uncomment out this code in the CSS panel to see this in action.

{% raw %}
dialog::backdrop {
background-color: purple;
opacity: 0.55;
filter: blur(100px);
}
{% endraw %}

<dialog> for Non-Modal Dialogs

The structure of a non-modal dialog element is the same as a modal dialog. The main difference is to show a non-modal dialog, you need to call the HTMLDialogElement show method.

With a non-modal dialog, the user is not blocked from navigating the rest of the page, i.e. no focus trapping, and the Escape key will not automatically close the dialog.

https://codepen.io/nickytonline/pen/ExMvNJw

Closing a dialog

To close a dialog or modal, we can use the HTMLDialogElement close method.

{% raw %}
const modal = document.querySelector("dialog");

// some button in the dialog that has a click event listener registered
modal.querySelector("button").addEventListener("click", () => {
modal.close();
});
{% endraw %}

Wrapping up

The web platform keeps getting better. It's great to see pain points in user land that had user solutions come natively to browser land.

References

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Migrating from Jest to Vitest for your React Application

· 5 min read
Nick Taylor
AI Engineer

Are you looking to migrate from Jest to Vitest for your React application? Look no further.

I recently migrated the OpenSauced app repository to Vitest. Here's the pull request if you're interested.

https://github.com/open-sauced/app/pull/2296

Why move from Jest to Vitest?

Both Jest and Vitest are great testing frameworks, so why bother switching?

Vitest supports ECMAScript modules (ESM), TypeScript out of the box.

Jest requires additional setup for both, although there is experimental support for ESM.

Vitest is also fast. Yes, it depends, but in general, it's faster. (See the Vitest comparison with other test runners)

Neo fighting an agent in the Matrix movie with one hand

If you're already using Vite in your project or the meta-framework you're using is based on Vite, using Vitest is a no-brainer as you're already in the Vite ecosystem.

If your project isn't using Vite, e.g. Next.js, it's still a great move.

Vitest makes it effortless to migrate from Jest. It supports the same Jasmine like API.

TLDR; You don't need to update existing tests, as it’s mostly a drop-in replacement for Jest.

Some other niceties are a default watch mode care of Vite instant Hot Module Reload (HMR).

Install Vitest

The first thing you want to do is install Vitest.

https://github.com/vitest-dev/vitest

Run npm install vitest -D in the terminal to install Vitest as a dev dependency.

Next up, create a vitest.config.ts file in the root of your project. Even if you're not using TypeScript, name it vitest.config.ts.

In that file, add the following code and save it.

{% raw %}
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
test: {
// some paths to the files that are test files
include: ["./**/*.test.ts", "./**/*.test.tsx"],
},
});
{% endraw %}

You can explicitly import describe, it/test, expect or you can have it work like in Jest where they're all globals. All you need to do is set globals to true in the Vitest configuration.

{% raw %}
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
test: {
include: ["./**/*.test.ts", "./**/*.test.tsx"],
+ globals: true,
},
});
{% endraw %}

Using Vitest with React

At OpenSauced, we're using Next.js to build out the main application.

Vitest is based off Vite which supports React via their plugin ecosystem, so you'll need to install the Vite React plugin to get React support.

Run npm install @vitejs/plugin-react -D to install the plugin as a dev dependency.

Update the Vitest configuration to add the React plugin.

{% raw %}
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
+ plugins: [react()],
test: {
include: ["./**/*.test.ts", "./**/*.test.tsx"],
globals: true,
},
});
{% endraw %}

React Testing Library

If you happen to be using React Testing Library in your project, you'll need to keep the jsdom dev dependency installed.

Next, add jsdom to your Vitest configuration.

{% raw %}
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
include: ["./**/*.test.ts", "./**/*.test.tsx"],
globals: true,
+ environment: "jsdom",
},
});
{% endraw %}

Aliases

Your project might be using aliases for paths. For example, in the OpenSauced app repository, components, lib, and img are aliases to folders.

If you need to support aliases, Vitest has you covered.

Here's an example of supporting the above-mentioned aliases.

{% raw %}
export default defineConfig({
plugins: [react()],
+ resolve: {
+ alias: {
+ components: fileURLToPath(new URL("./components", import.meta.url)),
+ lib: fileURLToPath(new URL("./lib", import.meta.url)),
+ img: fileURLToPath(new URL("./img", import.meta.url)),
+ },
+ },
test: {
include: ["./**/*.test.ts", "./**/*.test.tsx"],
globals: true,
environment: "jsdom",
},
});
{% endraw %}

TypeScript Types

If you're using TypeScript, you can add the types for Vitest to the project.

In your tsconfig.json file, add the types in the compiler options section of the TypeScript configuration file.

{% raw %}
{
"compilerOptions": {
// . .. other compiler options in your project
+ "types": ["vitest/globals"]
}

// . .. other TypeScript configuration options in your project
}

{% endraw %}

Running Tests

To run tests using Vitest, you can run vitest. By default, it will go into watch mode. If you only want to run the test suite once, e.g. for the CI/CD pipeline, run vitest run.

Removing Jest

If your project is a TypeScript project, you probably have the types for Jest in your project. If you do, run the following to remove the Jest TypeScript types.

{% raw %}
npm uninstall -D @types/jest
{% endraw %}

Uninstall Jest itself.

{% raw %}
npm uninstall jest jest-environment-jsdom -D
{% endraw %}

And that's it! Happy testing!

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.

Boost productivity with the GitHub CLI

· 10 min read
Nick Taylor
AI Engineer

The GitHub CLI is an indispensable tool as a project maintainer or contributor on GitHub. It can boost your productivity when getting things done.

Someone's head exploding like the exploding head emoji

The day my brain exploded was when I discovered (spoilers) that you could create a pull request using the GitHub CLI.

Let's get started!

Install the GitHub CLI

Head on over to the installation docs to get the GitHub CLI set up. There are installers for Linux, Windows, and macOS.

Log In to GitHub via the GitHub CLI

You're up and running but if you try to run any commands, you're going to be prompted to log in, so let's do that first.

Trying to execute a GitHub CLI command when not logged in results in the following message, To get started with GitHub CLI, please run: gh auth login
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.

To log in to GitHub via the GitHub CLI, run gh auth login.

GitHub CLI gh auth login command running

You'll be given two options for logging in. GitHub.com or GitHub Enterprise Server. In most cases, unless your company uses GitHub Enterprise Server, you'll select the default, GitHub.com.

Next, you'll be asked which protocol to log in with. The default is HTTPS, but I recommend SSH. To learn more about configuring GitHub with SSH, see Connecting to GitHub with SSH.

Login via SSH

The GitHub CLI prompting with the following, What is your preferred protocol for Git operations? Use arrows to move, type to filter HTTPS or SSH

Next, it will ask you to publish your public key to GitHub. This is safe to do and you can proceed.

GitHub CLI prompting to upload your public SSH key

It will prompt for a title for the key. Using the default value of "GitHub CLI" is fine.

The GitHub CLI prompting for a title for the SSH public key

Login via HTTPS

If you choose HTTPS, you'll be asked to authenticate Git with your GitHub credentials.

The GitHub CLI prompting to log in with your GitHub credentials

Press ENTER to continue.

Finishing Login Process

Next, you'll be prompted to log in via the browser or a token. To be honest, I've never used a token at this step. I always log in via the browser. If you have a token, go for it.

The GitHub CLI prompting to log in to GitHub via a browser or a token

You'll be given a code in the CLI that you need to copy (changed to some code in my screenshot) and then press ENTER to log in via the browser.

The GitHub CLI outputting a code you need to copy to finish the login process

Paste or type in the code and press the Continue button.

GitHub.com device activation screen

Next, you'll be asked to Authorize GitHub. Click the Authorize GitHub button.

The authorize GitHub CLI screen on github.com

At this point, depending on how you have the security of your account set up, you may be asked to log in via the GitHub mobile app.

Multifactor confirm access screen using GitHub mobile

Log in via the GitHub mobile app or other multifactor authentication methods you have set up.

At this point, you should be all set up.

GitHub.com confirmation screen that the device was connected successfully

And if you go back to the command line, you should see something similar to this.

GitHub CLI confirming that you are logged in

Useful Commands

Let's walk through a couple of commands I use every day, and then we'll check out some other useful ones that I use less frequently.

Reviewing a Pull Request

As a maintainer of a project, you will definitely be reviewing PRs (for external contributors or team members). Before we had the GitHub CLI, I always had to Google how to get someone's PR on my local machine with Git. I forgot all the time, so, at one point, I made a Git alias for it. The command looks like this, git fetch origin pull/pr_number/head:name_of_branch. So if I was going to review pull request 1234, the command would look something like this, git fetch origin pull/1234/head:pr-1234. You can call the branch whatever you want. I used to name it pr- with the number of the PR.

None of that is necessary these days. With the GitHub CLI, all you need to do is cd into the project directory in your terminal and then run gh co pr-number, e.g. gh co 2062

Here it is in action for a recent pull request I reviewed for the OpenSauced app repository.

Running the GitHub CLI checkout command, gh co 2062, to check out pull request 2062 from a repository

Creating a Pull Request

Before the GitHub CLI, I used to push my branch to GitHub, and then I would go to the repository's page on GitHub.com and create a pull request from there.

A repository's main page on github.com with a call to action to create a pull request from a branch pushed to github.com

Although that works, when I discovered that the GitHub CLI could do this, I was blown away. All you need to do is run gh pr create from the command line, assuming you're currently on the branch of the repo you want to associate with the pull request. You can provide additional arguments, e.g. gh pr create --draft or the shorter version gh pr create -d, but typically, when I'm creating a PR, I go through the steps in the CLI and continue the final step in the browser. It's a preference, so do what works best for you.

Here's me creating a new test PR.

Running the GitHub CLI create pull request command, gh pr create, to create a new pull request for a repository

Creating or Pushing a New Repository to GitHub

In the past, I always used to create a new repository from GitHub.com.

User menu on GitHub.com open with the menu item New Repository highlighted

I'm sure there is a way to create a repository on GitHub from the command line, but I never bothered to learn it, and now I don't really need to thanks to the GitHub CLI.

Create a Repository from Scratch

To create a repository from scratch, run gh repo create from the command line.

The GitHub CLI prompting user what to do with Create a new repository from scratch selected

Select Create a new repository on GitHub from scratch and press the ENTER key.

Next, you'll be prompted to name the repository, e.g. test.

The GitHub CLI prompting for a name for the repository

Next, choose the repository owner. If you're a part of one or more GitHub organizations, they will appear in the list. For our example, I will go with my own account, nickytonline as the repository owner.

The GitHub CLI prompting for a repository owner

Add a description for the repository, e.g. test or leave it blank. It's not required.

The GitHub CLI prompting for a description

Next, set the visibility of the repository. It can be public (default), private, or internal.

The GitHub CLI prompting for the visibility of the repository

Since this is a test repository, I'm going to set it to private.

The GitHub CLI running with privacy selected for the visibility

Next, you'll be asked to create a README file. Type y and press the ENTER key.

The GitHub CLI prompting to create a README

You'll be prompted to add a gitignore file. Type y and press the ENTER key.

The GitHub CLI prompting for to create a gitignore file

Next, choose the language that will best reflect the contents of the gitignore file. I do a lot of JavaScript, Node.js and TypeScript, so I'm going to choose Node.

Node selected as the language for the gitignore template

You'll be asked to add a license. Type y and press the ENTER key.

The GitHub CLI prompting to create a license

Choose the license that makes the most sense for the project you're creating. For the purposes of this blog post, I'll choose the MIT license.

MIT License selected in the GitHub CLI

A quick check will ask if you want to create the repository on GitHub. Type y and press the ENTER key to proceed.

The GitHub CLI summarizing all the selections made, prompting the user to create the repository on GitHub

Next, you'll be asked if you want to clone the repository locally.

The GitHub CLI asking the user if they want to clone the repository

Type y and press the ENTER key to proceed.

The new repository is on GitHub.com now and has been cloned on your machine.

Push an Existing Local Repository to GitHub

To push an existing local repository to GitHub, run gh repo create from the command line.

The GitHub CLI, with the Push an existing local repository to GitHub option selected

You'll be prompted for the path to the local repository. It defaults to ., the current directory. If, for some reason, you ran the command outside your local git repository folder, specify the folder to your repository.

The GitHub CLI prompting to enter the path to a local repository

Next, you'll be asked to name the repository. By default, it will use the name of the folder the local repository resides in, e.g. test. Change it if it makes sense to.

The GitHub CLI, prompting a user to enter a repository name

Next up, you're prompted to select a repository owner. By default, it's your user, e.g. nickytonlin, but you can select any organizations you're a part of as well.

The GitHub CLI prompting for a repository owner

Next, you'll be asked to add a description. You can add one or leave it blank. It's up to you.

The GitHub CLI prompting for a description for the repository

Next, you'll be asked to set the visibility of the repository. It can be public (default), private, or internal.

The GitHub CLI prompting to select the visibility of the repository

Next, you'll be asked if you want to set a remote. Press enter to say yes (the default)

The GitHub CLI prompting to add a remote for the repository

You'll be asked what the new remote should be called. Press the ENTER to accept the default name of origin. The GitHub CLI notifies you that the remote has been added, e.g. git@github.com:nickytonline/test.git

The GitHub CLI prompting to name the remote

And finally, you'll be asked if you want to push the commits from the current branch to the origin remote. Press the ENTER key to push the commits, and you're done!

The GitHub CLI pushing the current branch to the origin remote on GitHub

Conclusion

For myself, the GitHub CLI has become a game changer in my day-to-day workflow. I literally use it every day, well, work days. 😎

From creating a new repository, to pulling down a pull request (PR) to creating a PR and more, the GitHub CLI has become indispensable to me.

There is a whole other set of commands available in the GitHub CLI that I encourage you to check out and that, to be honest, even I should explore further.

I realize not everyone is comfortable with the command line, but I think that if you give the GitHub CLI a chance, you may grow to love it. As always, though, use the tools that make you the most productive.

Stay saucy peeps!

If you would like to know more about my work in open source, follow me on OpenSauced.