11.3 · difficulty 2/4 · 16 min read
Project 9: Replicate This Site's Monorepo
Build the same monorepo structure used by vite-mastery from scratch — a three-tier apps/ + packages/ + content/ architecture with a complete Vite 8 + Next.js + pnpm workspace configuration.
Vite 8.1Stable
Project Structure Design
text
my-monorepo/
├── apps/
│ └── web/ ← main application (Next.js)
│ ├── package.json
│ ├── next.config.ts
│ └── src/
├── packages/
│ ├── ui/ ← shared UI components
│ │ ├── package.json
│ │ ├── vite.config.ts ← library mode build
│ │ └── src/
│ └── utils/ ← shared utility functions
│ ├── package.json
│ ├── vite.config.ts
│ └── src/
├── pnpm-workspace.yaml
├── package.json ← root package.json
└── tsconfig.json ← root tsconfig (references only)Root Configuration Files
pnpm-workspace.yaml
yaml
packages:
- "apps/*"
- "packages/*"Root package.json
json
{
"name": "my-monorepo",
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm --filter @my/web dev",
"build": "nex build",
"typecheck": "tsc --noEmit",
"lint": "oxlint .",
"clean": "next clean"
},
"devDependencies": {
"typescript": "^6.0.0",
"oxlint": "^1.0.0",
"next:" "^16.2.9"
}
}Root tsconfig.json (references aggregator)
json
{
"files": [],
"references": [{ "path": "./packages/ui" }, { "path": "./packages/utils" }, { "path": "./apps/web" }]
}packages/utils Configuration
The simplest utility package:
ts
import { defineConfig } from "vite"
import { resolve } from "node:path"
export default defineConfig({
build: {
lib: {
entry: resolve("src/index.ts"),
formats: ["es", "cjs"],
fileName: (f) => `index.${f === "es" ? "js" : "cjs"}`,
},
},
})json
{
"name": "@my/utils",
"version": "0.0.1",
"type": "module",
"exports": {
".": {
"import": "./src/index.ts",
"types": "./src/index.ts"
}
},
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist"
},
"devDependencies": {
"vite": "^8.1.0",
"typescript": "^6.0.0",
"rimraf": "^6.0.0"
}
}packages/ui Configuration
React component library package:
json
{
"name": "@my/ui",
"type": "module",
"exports": {
".": {
"import": "./src/index.tsx",
"types": "./src/index.tsx"
}
},
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@my/utils": "workspace:*"
},
"peerDependencies": {
"react": ">=18"
},
"devDependencies": {
"@vitejs/plugin-react": "^6.0.0",
"react": "^19.0.0",
"vite": "^8.1.0",
"typescript": "^6.0.0",
"rimraf": "^6.0.0"
}
}apps/web Configuration
ts
import type { NextConfig } from "next"
const config: NextConfig = {
// Process workspace packages through Next.js's webpack
// (so their TypeScript source gets compiled)
transpilePackages: ["@my/ui", "@my/utils"],
}
export default configDX Optimization: Alias to src
In a Vite project, use aliases to point both the IDE and HMR directly to source:
ts
import { resolve } from "node:path"
export default defineConfig({
resolve: {
alias: {
"@my/ui": resolve("../../packages/ui/src"),
"@my/utils": resolve("../../packages/utils/src"),
},
},
})Running the Project
bash
cd examples/monorepo-template
pnpm install
pnpm dev # start apps/webSelf-check
- Why does the root
tsconfig.jsonusereferencesinstead of directly configuring all files? - What does Turborepo's
dependsOn: ["^build"]mean? - What does
transpilePackagesdo in Next.js? Why is it needed? - If you use only
pnpm -r run buildwithout Turborepo, what problems can arise?
ts
// Extend this monorepo: add a packages/icons package
// - Contains SVG icons, each icon exported as a React component
// - Icons are auto-generated from SVG files (via a script)
// - Consumer usage: import { ArrowIcon } from "@my/icons"
//
// Questions:
// 1. How should packages/icons/package.json be configured?
// 2. What special handling does vite.config.ts need? (SVG → React component)
// 3. If there are many icons (500+), how do you optimize tree-shaking
// so consumers only bundle the icons they actually use?