Thilo Maier

Configuring Turborepo for a SvelteKit monorepo

Thilo Maier •
Last modified:

Recently, I converted the repository for my website to a Turborepo. Turborepo is a task dependency management layer on top of a package manager, and it works for normal repositories and monorepos. The underlying package manager has to be one of NPM, pnpm, or Yarn, all of which come with workspaces support.

The central pitch of Turborepo is to speed up workspace tasks, primarily builds. After configuring task dependencies in one or more turbo.json, Turborepo uses this information to run tasks in parallel with an aggressive caching strategy. Turborepo can complement local caching with shared remote caching. Often this will significantly reduce the duration of deployment builds because local builds are cached remotely and can be reused for deployment builds.

Workspaces configuration

Let's look at how I configured the repository for my website with the following monorepo directory structure:

maier.tech
├── apps
│   └── website
└── packages
    └── ui
File tree for the maier.tech monorepo.

I use NPM as a package manager. Therefore, I defined my workspaces with the workspaces property:

{
	"private": true,
	"workspaces": [
		"apps/*",
		"packages/*"
	],
	"devDependencies": {
		"turbo": "^1.8.8"
	}
}
package.json

I recommended adding package turbo to devDependencies, which gives you control over which version to use. You should also install the turbo package globally to make the turbo command available in your terminal. A globally installed turbo command will use the Turborepo version declared in devDependencies.

Turborepo configuration

The Turborepo configuration is in turbo.json at the project root level. For my monorepo, it looks like this:

{
	"$schema": "https://turbo.build/schema.json",
	"pipeline": {
		"build": {
			"dependsOn": ["^build"],
			"outputs": [".svelte-kit/**", ".vercel/**"]
		},
		"check": {},
		"lint": {},
		"dev": {
			"cache": false,
			"persistent": true
		}
	}
}
turbo.json

The pipeline property describes dependencies between NPM tasks (defined in the scripts tags of workspace package.json files). Each task can have additional properties, which you can look up in the Turborepo docs (configuration options) . I will highlight two of them:

  1. "dependsOn": Value ["^build"] means that every build task should run the build tasks of dependencies that reside inside the monorepo in other workspaces.
  2. "outputs": Describes build artifacts that Turborepo should cache. E.g., the build of a SvelteKit app goes into directory .svelte-kit. If Turborepo figures out nothing has changed during a build, it will retrieve .svelte-kit from its cache instead of running the task. Similarly, the output of adapter-vercel goes into the .vercel directory and needs to be cached. The best case for a build is that Turborepo can fetch both .svelte-kit and .vercel from the cache.

Nested configuration

Starting with Turborepo v1.8, you can nest configurations and complement a project-level configuration with workspace-specific configurations. E.g., in my monorepo, packages/ui contains a UI package that is managed with SvelteKit's package tooling. When you run the build command for ui, the build artifacts go into dist and must be cached. I could add dist to the outputs property of the build task in the project root turbo.json. Then they would be applied to every build task in every workspace.

As an alternative, I created the file packages/ui/turbo.json, which extends the project root turbo.json:

{
	"extends": ["//"],
	"pipeline": {
		"build": {
			"outputs": ["dist/**", ".svelte-kit/**", ".vercel/**"]
		}
	}
}
packages/ui/turbo.json

Turborepo permits overrides only for anything under thepipeline property. The above turbo.json build task inherits all properties from the project root turbo.json and overrides only the outputs property.

Pitfall: Getting the outputs wrong

Getting the outputs wrong can have unintended side effects. After refactoring my repository to a monorepo, I got the following error for every Vercel deployment:

Screenshot of the error log of a Vercel deploy. The error message reads: No output directory named 'public' found after the build completed.
Vercel build error after migrating my repository to a Turborepo.

By default, Vercel expects the deployment files in the public directory or another special directory, e.g., .vercel. I had forgotten to add .vercel/** to outputs in turbo.json. Whenever Turborepo determined that it could reuse a cached build, it did not run the build task and instead fetched all outputs it was aware of from the cache. Since it did not know about .vercel, it could not fetch it from the cache, and the build ended without a valid build directory.

Pitfall: Persistent tasks

Persistent tasks are long-running, e.g., the dev task is persistent. Turborepo does not allow any task to depend on a persistent task because it blocks subsequent tasks. Imagine ui/package.json defines a watch task that builds the library whenever a file changes. I want to add this configuration to apps/website/turbo.json:

// ...

"pipeline": {
  "dev": {
    "dependsOn": ["ui#watch"]
  }
}

// ...
apps/website/turbo.json

But this is not permitted since Turorepo does not allow the dev task to depend on watch, which is a persistent task. Instead of defining the watch task as a dependency of the dev task in turbo.json, you need to launch the watch task at the NPM level:

{
  	// ...

  	"scripts": {
    	"dev": "npm run watch -w=ui & vite dev",

      // ...
    }

    // ...
  }
apps/website/package.json

This script simultaneously launches the watch task for packages/ui and the dev task for apps/website.

Conclusion

Configuring Turborepo for a monorepo with one or more SvelteKit apps is relatively easy. But you must get the outputs right, including any directories specific to your hosting provider. As a layer on top of a package manager, Turborepo does not replace NPM, pnpm, or Yarn. As you have seen in the above examples, some configurations go in a turbo.json, and others go in a package.json. Knowing where configurations go can be challenging in the beginning. Expect Turborepo to support more configurations over time and perhaps even merge with a package manager or another build tool.