Scaffolding a monorepo with multiple apps and shared packages
Build a production-ready Turborepo monorepo with SvelteKit apps, shared component libraries, and centralized configuration using pnpm workspaces.
This architecture demonstrates how to scale SvelteKit development across multiple applications while maintaining consistent tooling, shared components, and optimized build pipelines. The approach leverages Turborepo’s intelligent caching and task orchestration alongside pnpm’s efficient workspace management.
Repository Structure
The monorepo separates consumer applications from internal packages, creating clear boundaries between public-facing code and shared infrastructure:
apps/
demo/ - Main SvelteKit app with Tailwind CSS v4
docs/ - Documentation site with mdsvex
packages/
components/ - Shared Svelte 5 component library
shared/ - Utilities: cn(), formatters
eslint-config/ - Shared ESLint 9 flat config
typescript-config/ - Shared TypeScript configs
tailwind-config/ - Shared Tailwind v4 styles/theme Applications Layer
Both applications use SvelteKit with identical tooling but serve different purposes. The demo app showcases component usage and interactive examples, while the docs app extends SvelteKit with mdsvex for markdown-based documentation, enabling rich technical writing with Shiki syntax highlighting and custom remark/rehype plugins.
Package Layer
Internal packages fall into two categories: runtime dependencies (components, shared utilities) and development dependencies (config packages). Runtime packages ship actual code consumed by apps, while config packages standardize tooling across the workspace.
Shared Configuration Strategy
Centralizing configuration prevents drift and ensures all workspace members follow identical patterns. Each config package exports reusable settings consumed by apps and other packages.
TypeScript Configuration
The @repo/typescript-config package provides two base configurations:
base.json - For libraries and utilities:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022"],
"strict": true,
"declaration": true
}
} Used by @repo/shared to generate type declarations for exported utilities.
svelte.json - For SvelteKit applications:
{
"extends": "./base.json",
"compilerOptions": {
"target": "ESNext",
"allowJs": true,
"sourceMap": true,
"types": ["svelte"]
}
} Consumed by apps and @repo/components via:
{
"extends": "@repo/typescript-config/svelte.json"
} ESLint Configuration
Modern ESLint 9 uses flat config arrays instead of legacy .eslintrc files. The shared config exports a composable array:
import tseslint from 'typescript-eslint'
import eslintPluginSvelte from 'eslint-plugin-svelte'
export const config = [
...tseslint.configs.recommended,
...eslintPluginSvelte.configs['flat/recommended'],
{
rules: {
'@typescript-eslint/no-unused-vars': 'warn'
}
}
] Apps import and extend this config:
import { config } from '@repo/eslint-config/index.js'
export default [
...config,
{
ignores: ['.svelte-kit/**', 'build/**']
}
] Tailwind CSS v4 Configuration
Tailwind v4’s new architecture uses CSS imports instead of JavaScript config files. The @repo/tailwind-config package exports a centralized stylesheet:
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';
@theme {
--font-family-sans: 'Inter', system-ui, sans-serif;
--font-family-mono: 'JetBrains Mono', monospace;
} Apps consume this via a single import:
@import '@repo/tailwind-config'; The @tailwindcss/vite plugin handles dynamic CSS generation during development and optimized production builds.
Component Library Architecture
The @repo/components package demonstrates headless UI patterns using bits-ui primitives with Svelte 5’s modern reactivity system.
Package Structure
Components export as named modules, allowing tree-shaking and selective imports:
{
"exports": {
"./Button": "./src/lib/Button.svelte",
"./Dialog": "./src/lib/Dialog/index.js",
"./next": "./src/lib/next/index.js"
}
} Apps import specific components:
<script>
import Button from '@repo/components/Button'
import { Dialog, DialogTitle, DialogBody } from '@repo/components/Dialog'
</script> Development Workflow
The library uses @sveltejs/package in watch mode during development:
{
"scripts": {
"dev": "svelte-package -w",
"build": "svelte-package"
}
} Running pnpm --filter @repo/components dev enables hot module replacement—changes to components immediately reflect in consuming apps without manual rebuilds.
Composition Patterns
Complex components use composition over configuration. Instead of a monolithic <Dialog> with 20 props, smaller subcomponents provide flexibility:
<Dialog bind:open>
<DialogTitle>Confirm Action</DialogTitle>
<DialogBody>
<p>This operation cannot be undone.</p>
</DialogBody>
<DialogActions>
<Button on:click={() => (open = false)}>Cancel</Button>
<Button variant="danger">Confirm</Button>
</DialogActions>
</Dialog> Each subcomponent handles its own styling and behavior while coordinating through Svelte contexts.
Shared Utilities
The @repo/shared package provides framework-agnostic utilities compiled to JavaScript for universal consumption.
Class Name Merging
The cn() function combines clsx and tailwind-merge:
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
} This handles Tailwind class conflicts intelligently:
cn('px-4 py-2', 'px-6')
// Result: 'py-2 px-6' (px-4 is removed) Formatters
Locale-aware formatters provide consistent data presentation:
export function formatNumber(value: number, locale = 'en-US'): string {
return new Intl.NumberFormat(locale).format(value)
}
export function formatDate(date: Date, locale = 'en-US'): string {
return new Intl.DateTimeFormat(locale).format(date)
}
export function formatRelativeTime(
value: number,
unit: Intl.RelativeTimeFormatUnit,
locale = 'en-US'
): string {
return new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }).format(value, unit)
} Turborepo Pipeline Configuration
Turborepo orchestrates tasks across the workspace with dependency-aware execution and intelligent caching.
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".svelte-kit/**", ".vercel/**", "dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"check": {
"dependsOn": ["^build"]
}
}
} Dependency Graph Execution
The ^build notation creates a dependency graph. Running pnpm build from the root:
- Builds
@repo/shared(no dependencies) - Builds
@repo/components(depends on shared) - Builds apps in parallel (depend on components)
Turborepo caches outputs based on input fingerprints. If @repo/shared hasn’t changed, its build is skipped and cached artifacts are restored.
Development Mode
The dev task runs multiple processes concurrently:
pnpm dev
# Starts:
# - apps/demo dev server
# - apps/docs dev server
# - packages/components in watch mode Changes to components automatically trigger HMR in consuming apps without full rebuilds.
Workspace Dependencies
Internal packages use the workspace:* protocol, which pnpm resolves to local workspace versions:
{
"dependencies": {
"@repo/components": "workspace:*",
"@repo/shared": "workspace:*"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/tailwind-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
} During publishing, pnpm replaces workspace:* with actual version ranges. For local development, it symlinks packages, enabling instant updates without reinstallation.
Running Workspace Commands
Turborepo provides global task execution, while pnpm filters enable targeted operations.
Global Tasks
Run tasks across all workspaces:
pnpm build # Build everything
pnpm lint # Lint all packages
pnpm check # Type-check all packages Filtered Tasks
Target specific workspaces:
pnpm --filter demo dev # Start demo app
pnpm --filter @repo/components build # Build components
pnpm --filter docs check # Type-check docs app Multiple Filters
Combine filters for batch operations:
pnpm --filter "./apps/*" build # Build all apps
pnpm --filter "@repo/*" lint # Lint all packages Technology Choices
Why Turborepo Over Nx or Rush
Turborepo prioritizes simplicity and speed. Unlike Nx’s opinionated generators or Rush’s strict publishing workflows, Turborepo focuses solely on task orchestration and caching. The entire configuration fits in 20 lines of JSON.
Why pnpm Over npm or Yarn
pnpm’s symlink-based approach saves gigabytes of disk space and enables instant cross-package changes. The workspace:* protocol provides better local development ergonomics than npm workspaces or Yarn’s portal: protocol.
Why Tailwind CSS v4
Version 4’s CSS-based configuration eliminates JavaScript config files and enables better IDE support. The new @tailwindcss/vite plugin generates CSS on demand, reducing build times by 40-60% compared to v3’s PostCSS pipeline.
Why ESLint 9 Flat Config
Flat config arrays are easier to compose and debug than legacy cascading configs. TypeScript support is first-class, and the new config format enables better IDE integration and error messages.
Best Practices
Keep Packages Focused
Each package should solve one problem. @repo/shared handles utilities, not components. @repo/eslint-config handles linting, not TypeScript. Clear boundaries prevent circular dependencies and make packages easier to reason about.
Version Configuration Packages Together
When updating ESLint or TypeScript, bump the config package version and update dependents simultaneously. Mismatched config versions create subtle bugs that are difficult to debug.
Use Type Imports for Shared Types
When packages share types, use import type to avoid circular runtime dependencies:
import type { ButtonProps } from '@repo/components/Button' This ensures type-only imports are erased during compilation.
Leverage Turborepo Caching
Structure tasks to maximize cache hits. Separate type checking from builds, keep test files colocated with source, and avoid tasks that generate timestamps or random IDs.
This architecture demonstrates how modern tooling—Turborepo, pnpm, Tailwind v4, ESLint 9—can create a maintainable monorepo foundation. The key is balancing shared infrastructure with application flexibility, centralizing configuration without creating bottlenecks, and optimizing for both development speed and production quality.