vite-mastery

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 config

DX 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/web

Self-check

  1. Why does the root tsconfig.json use references instead of directly configuring all files?
  2. What does Turborepo's dependsOn: ["^build"] mean?
  3. What does transpilePackages do in Next.js? Why is it needed?
  4. If you use only pnpm -r run build without 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?