🏎️💨 Turbocharge your builds with a Turborepo remote cache in a single edge function
Matt Kane
Posted on November 15, 2023
Monorepos are a great tool for organizing projects, but they can lead to slow build particularly in complex projects with lots of interdependent packages. Turborepo is a great solution for improving your build times which has become particularly popular for Next.js sites. It uses lots of clever tricks to ensure you only build the absolute minimum needed, using lots of caching and dependency tracking. I'm going to share how I built a custom Turborepo remote cache in a single edge function.
The secrets of Full Turbo
One of Turborepo's neatest tricks is a global remote cache, which lets you share build artifacts between your local development environment and production builds and between different members of your team. It works in the background to speed up your builds everywhere.
Turborepo is owned by Vercel, so understandably they want you to use their cache service. However Turborepo does support custom remote caching, and there is already a great open source Turborepo remote cache project, but I had a feeling I could build something simpler.
A cache server in 100 lines of code
While the remote cache server API is undocumented, it is fortunately quite simple. I had early access to the new Netlify Blobs product, which seemed perfect for this. With an Edge Function handling the API and Netlify Blobs for the storage, I implemented a Turborepo remote cache in under 100 lines of code.
If you just want to start speeding up your builds, you can check out the repo or just click the button:
If you want to see how I built it, read on.
The Turborepo remote cache API
The docs for the Turborepo custom cache currently consists of a link to the Go source of the client, but the API is fortunately very simple. A Turborepo remote cache needs an endpoint at /v8/artifacts/:hash
that accepts PUT
requests to add an item, and GET
requests to return it. Authentication is via a shared bearer token, and the teamId
is passed as a query param. This is super simple to set up as a Netlify edge function:
import type { Config } from "@netlify/edge-functions";
export default async function handler(request: Request, context: Context) {
// Do cool stuff here
}
export const config: Config = {
method: ["GET", "PUT"],
path: "/v8/artifacts/:hash",
// This lets us handle our own cache rules
cache: "manual",
};
This gives us a function that runs on the right path, with the right methods.
Now add some auth. Turborepo sends the token in an Authorization
header, which we can store in a shared environment variable. You can use anything for the token value. I like to use a UUID, which you can generate on a Mac by running uuidgen
in the terminal. You'll need to store the same token on the server site and any repos that use it.
// snip
export default async function handler(request: Request, context: Context) {
const bearerHeader = request.headers.get("authorization");
// Remove the leading "Bearer " from the token
const token = bearerHeader?.replace("Bearer ", "");
// Compare it with the stored value, and return a 401 if it doesn't match
if (!token || token !== Netlify.env.get("TURBO_TOKEN")) {
console.log("Unauthorized");
return new Response("Unauthorized", { status: 401 });
}
}
We're going to use Netlify Blobs to store our artifacts. These don't need any setup - you can just start using them. We'll use a store based on the team ID:
import { getStore } from "@netlify/blobs";
import type { Context } from "@netlify/edge-functions";
export default async function handler(request: Request, context: Context) {
// Auth stuff goes here...
const url = new URL(request.url);
const teamId = url.searchParams.get("teamId") ?? "team_default";
// Get a store
const store = getStore(`artifacts-${encodeURIComponent(teamId)}`);
Now we just need to handle getting and putting the artifacts. First we generate the cache key for the object and then handle uploads:
const hash = context.params?.hash || url.pathname.split("/").pop();
if (!hash) {
console.log("Missing hash");
return new Response("Not found", { status: 404 });
}
const key = encodeURIComponent(hash);
if (request.method === "PUT") {
// Get the uploaded file as binary
const blob = await request.arrayBuffer();
if (!blob) {
console.log("No content");
return new Response("No content", { status: 400 });
}
// ...then put it in the store. That's all!
await store.set(key, blob);
return new Response("OK");
}
Now we've implemented uploads, we can implement downloads.
try {
// Try to get the blob from the store as a binary buffer
const blob = await store.get(key, {
type: "arrayBuffer",
});
if (!blob) {
// Oh no, return a 404
return new Response(`Artifact ${hash} not found`, { status: 404 });
}
const headers = new Headers();
// This content-type is required by Turborepo
headers.set("Content-Type", "application/octet-stream");
headers.set("Content-Length", blob.byteLength.toString());
// We can set some useful cache headers, using Netlify's new cache features
headers.set(
"Netlify-CDN-Cache-Control",
"public, s-maxage=31536000, immutable"
);
headers.set("Netlify-Vary", "header=Authorization,query=teamId");
return new Response(blob, { headers });
} catch (e) {
// Catch any errors and return a 500
console.log(e);
return new Response(e.message, { status: 500 });
}
That's it!
If you add that to your site at netlify/edge-functions/turbofan.ts
then it will run and handle requests to the cache URL.
Easy mode
Instead of manually writing or copying the code, you can just use the version I've published to deno.land:
import type { Config } from "@netlify/edge-functions";
export { handleRequest as default } from "https://deno.land/x/turbofan/mod.ts";
export const config: Config = {
method: ["GET", "PUT"],
path: "/v8/artifacts/:hash",
cache: "manual",
};
That's all you need. Make sure you've set the TURBO_TOKEN
env var.
Or, even easier, just click this button to deploy a standalone remote cache:
Setting up your repo
You now need to tell turborepo
to use the remote cache. You can do this by creating a config file. This is not the turbo.json
in the root of your repo! You create it at .turbo/config.json
, which may have been excluded in your .gitignore
You will need to change that. Here's what you need to create:
{
"teamid": "team_anything_you_want_here",
"apiurl": "https://your-turboprop-server-name-here.netlify.app"
}
If you create that and set the TURBO_TOKEN
env var, Turborepo will know to use the remote cache.
You can see it's working because Turborepo will print "Remote caching enabled" in the logs.
Have fun, and may all your builds be FULL TURBO.
Posted on November 15, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 15, 2023