使用Turbopack、Biome、Next.js、Express、Tailwind CSS和ShadCN构建全栈Monorepo项目

这是一篇翻译,原文:www.thehalftimecode.com/building-a-...

github 地址: github.com/ivesfurtado...

简介

作为开发者或实习生,你可能遇到过在单个项目中管理多个应用程序或包的挑战。无论是同时开发前端应用和后端API,还是在项目的不同部分之间共享UI组件和实用函数,事情都可能很快变得难以管理。

这就是monorepo 的用武之地。通过将代码组织到一个包含多个应用和共享包的单一代码库中,你可以简化开发过程并改善协作。在本指南中,我们将带你使用 TurbopackBiomeNext.js 15Express.jsTailwind CSSShadCN 设置一个monorepo。我们还将使用pnpm作为包管理器来优化依赖管理。

本教程结束时,你将拥有一个功能完整的monorepo,其中包含两个应用程序(Next.js 和Express.js)和三个共享包(UI组件、TypeScript类型和实用函数)。让我们开始吧!

注意: 你可以在文章末尾找到仓库链接。如果你以前从未设置过 monorepo,我强烈建议通读整个教程,而不是仅仅 fork 仓库。

前提条件

在开始本教程之前,确保你已安装以下内容:


什么是Monorepo?

在开始构建之前,让我们明确什么是 monorepo 以及为什么它很有用。

定义

Monorepo 是一个包含多个项目或包代码的单一代码库。它不是为每个应用程序或共享库设置单独的代码库,而是将所有内容都放在一个地方。

使用Monorepo的好处

  1. 代码共享: 轻松在不同应用程序之间共享代码(例如UI组件或实用函数)。
  2. 一致性: 在所有项目中保持依赖项和配置的一致性。
  3. 简化协作: 由于所有内容都在一个地方,因此开发者在项目的不同部分之间协作更加容易。
  4. 原子更改: 在一次提交中对多个应用程序或包进行更改。
  5. 集中式CI/CD: 从一个地方管理持续集成和部署流程。

在本指南中,我们将创建一个包含以下内容的 monorepo(turborepo):

  • 一个Next.js 应用(前端)。
  • 一个Express.js 应用(后端)。
  • 用于UI组件(使用 Tailwind CSS + Shadcn/ui)、共享类型和实用函数的包。

为什么使用 pnpm 和 Turbopack ?

为了使我们的 monorepo 高效和可扩展,我们将使用两个关键工具:pnpm 进行包管理和Turbopack/Turborepo 进行快速构建。

pnpm

pnpm是npm和Yarn的替代品,提供了几个优势:

  • 更快的安装: pnpm通过使用硬链接而不是复制文件来更快地安装依赖项。
  • 磁盘空间效率: 它通过避免重复依赖项来节省磁盘空间。
  • 工作区支持: pnpm原生支持工作区,非常适合monorepo,你可以在其中拥有多个共享依赖项的项目。

Turbopack

Turbopack 是由 Vercel 为 Next.js 引入的新打包工具。它的设计比 Webpack 快得多,尤其是在开发过程中:

  • 更快的热模块替换(HMR): 当你进行更改时,Turbopack 通过仅重新加载必要的模块来加速开发。
  • 优化的生产构建: Turbopack 优化你的生产构建,使其更小更快。
  • 与 Next.js 15 无缝集成: Turbopack可以与 Next.js 新的应用目录结构开箱即用。

有了这些工具,让我们继续设置我们的项目结构。

项目结构概述

以下是我们最终的项目结构:

bash 复制代码
monorepo/
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── apps/
│   ├── web/       # Next.js应用
│   └── server/    # Express.js应用
├── packages/
│   ├── ui/        # 共享UI组件(使用Tailwind CSS + ShadCN)
│   ├── types/     # 共享TypeScript类型
│   ├── tsconfig/  # Typescript配置
│   └── utils/     # 共享实用函数
├── .gitignore     # 从git中取消跟踪文件
├── biome.json     # Biome配置
├── package.json   # 项目配置
├── turbo.json     # Turbopack配置
└── pnpm-workspace.yaml # pnpm工作区配置

我们将项目组织成两个主要目录:

  1. apps/:这将包含我们的两个主要应用程序---web(Next.js)和server(Express.js)。
  2. packages/:这将包含两个应用都可以使用的共享代码---ui用于共享UI组件,types用于TypeScript类型,utils用于共享实用函数,以及tsconfig用于typescript配置文件。

现在我们对结构有了一个概述,让我们开始设置 monorepo。


设置Monorepo

步骤1:使用pnpm工作区初始化Monorepo

首先,如果你还没有安装pnpm,需要全局安装:

接下来,创建你的主项目目录:

bash 复制代码
mkdir monorepo && cd monorepo

初始化一个新的工作区:

这个命令在项目根目录创建一个package.json文件。现在我们需要通过在根目录创建一个pnpm-workspace.yaml文件来告诉pnpm哪些目录应该是工作区的一部分:

vbnet 复制代码
packages:
  - 'apps/*'
  - 'packages/*'

这个配置告诉pnpm,apps/packages/内的任何文件夹都应被视为工作区的一部分。

步骤2:配置Turbopack

接下来,我们将通过在项目根目录创建一个turbo.json文件来配置Turbopack:

json 复制代码
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    "**/.env.*local"
  ],
  "tasks": {
    "topo": {
      "dependsOn": [
        "^topo"
      ]
    },
    "build": {
      "dependsOn": [
        "^build"
      ],
      "outputs": [
        "dist/**",
        ".next/**",
        "!.next/cache/**"
      ]
    },
    "lint": {
      "dependsOn": [
        "^topo"
      ]
    },
    "format": {
      "dependsOn": [
        "^topo"
      ]
    },
    "lint:fix": {
      "dependsOn": [
        "^topo"
      ]
    },
    "format:fix": {
      "dependsOn": [
        "^topo"
      ]
    },
    "check-types": {},
    "dev": {
      "cache": false,
      "persistent": true
    },
    "add-shadcn-component": {
      "dependsOn": [
        "^topo"
      ]
    },
    "clean": {
      "cache": false
    }
  }
}

这个配置定义了Turbopack应如何处理工作区中的构建。

注意add-shadcn-component命令,这是一个自定义命令,将在我们的UI包中使用,以便直接从根目录轻松添加 shadcn/ui 的新组件。

步骤3:全局配置

接下来,我们将更新根目录的package.json以添加脚本和依赖项。

json 复制代码
{
  "name": "monorepo",
  "private": true,
  "scripts": {
    "changeset": "changeset",
    "publish:packages": "changeset publish",
    "version:packages": "turbo build && changeset version",
    "add-shadcn-component": "turbo run add-shadcn-component -- --",
    "build": "turbo build",
    "dev": "turbo dev",
    "format": "turbo format --continue --",
    "format:fix": "turbo format --continue -- --write",
    "lint": "turbo lint --continue --",
    "lint:fix": "turbo lint --continue -- --apply",
    "clean": "turbo clean"
  },
  "dependencies": {
    "@changesets/changelog-github": "^0.5.0",
    "@changesets/cli": "^2.27.1",
    "turbo": "^2.1.3"
  },
  "devDependencies": {
    "@biomejs/biome": "^1.7.2",
    "typescript": "^5",
    "postcss": "^8.4.27"
  },
  "packageManager": "pnpm@9.12.1"
}

对于 Biome 配置,我们将创建一个名为biome.json的文件:

json 复制代码
{
    "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json",
    "files": {
      "ignoreUnknown": true,
      "ignore": [
        "node_modules/*",
        "*.config.*",
        "*.json",
        "tsconfig.json",
        ".turbo",
        "**/dist",
        "**/out",
        ".next"
      ]
    },
    "organizeImports": {
      "enabled": true
    },
    "linter": {
      "enabled": true,
      "rules": {
        "recommended": true,
        "complexity": {
          "noForEach": "off",
          "noUselessFragments": "off"
        },
        "correctness": {
          "useExhaustiveDependencies": "off",
          "noUnusedImports": "warn",
          "noUnusedVariables": "warn"
        },
        "style": {
          "noParameterAssign": "off"
        }
      }
    },
    "formatter": {
      "enabled": true,
      "formatWithErrors": false,
      "indentStyle": "space",
      "lineEnding": "lf",
      "lineWidth": 120
    }
  }
  

一个非常重要的文件是.gitignore,这是我们告诉Git我们不想跟踪哪些文件的文件。

.gitignore 复制代码
# dependencies
/node_modules
/.pnp
.pnp.js

node_modules
packages/*/node_modules
apps/*/node_modules
.next

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
.pnpm-debug.log*

# other lockfiles that's not pnpm-lock.yaml
package-lock.json
yarn.lock

# local env files
.env
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts


# intellij
.idea

dist/**
/dist
packages/*/dist

.turbo
/test-results/
/playwright-report/
/playwright/.cache/

这个配置定义了我们的项目默认设置。现在我们已经设置了工作区配置文件,让我们继续创建我们的应用程序。

步骤3:.vscode文件夹

项目目录中的.vscode文件夹存储了特定于 Visual Studio Code的 配置设置。这些设置允许你为你的项目或工作区需求个性化和优化 VS Code。以下是两种主要的设置类型:

  • 用户设置:全局应用于系统上所有 VS Code 实例。它们非常适合你想要保持一致的设置,比如字体大小或主题。
  • 工作区设置 :仅应用于当前项目。这对于项目特定的配置很有用,比如从文件浏览器中排除某些文件夹(例如node_modules)。

VS Code 使用 JSON 文件存储这些设置,使自定义和通过版本控制共享变得容易。为了便于管理,你可以直接在 JSON 文件中修改设置,或使用设置编辑器,它提供了一个方便的图形界面。

对于我们的项目,我们将创建两个存储这些配置的文件。首先,在根目录创建一个名为.vscode的文件夹。然后,创建extensions.json

json 复制代码
{
  "recommendations": [
    "yoavbls.pretty-ts-errors",
    "bradlc.vscode-tailwindcss",
    "biomejs.biome"
  ]
}
  

我们需要的最后一个配置是全局设置,所以创建一个名为settings.json的文件:

json 复制代码
{
  "editor.codeActionsOnSave": {
    "source.organizeImports.biome": "explicit",
    "source.fixAll.biome": "explicit",
  },
  "editor.defaultFormatter": "biomejs.biome",
  "editor.formatOnSave": true,
  "tailwindCSS.experimental.classRegex": [
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ],
  "typescript.enablePromptUseWorkspaceTsdk": true,
  "typescript.tsdk": "node_modules/typescript/lib",

  "typescript.preferences.autoImportFileExcludePatterns": [
    "next/router.d.ts",
    "next/dist/client/router.d.ts"
  ],
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "vscode.json-language-features"
  }
}

创建第一个包(tsconfig)

为了创建将在我们整个monorepo中使用的typescript配置以及web和server的单独配置,我们将创建我们的tsconfig包。

bash 复制代码
mkdir packages && cd packages && mkdir tsconfig && cd tsconfig

我们将有6个用于Typescript的配置文件:

  • base : base.json
  • web : next.json
  • server : express.json
  • ui : ui.json
  • utils : utils.json
  • types : types.json

首先我们将创建我们的package.json

json 复制代码
{
  "name": "@monorepo/tsconfig",
  "version": "0.0.0",
  "private": true,
  "license": "MIT",
  "publishConfig": {
    "access": "public"
  }
}

然后我们将创建我们的base.json配置文件(你可以在这里找到所有tsconfig设置):

json 复制代码
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "alwaysStrict": false,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "resolveJsonModule": true,
    "target": "ESNext",
    "lib": [
      "DOM",
      "DOM.Iterable",
      "ESNext"
    ],
    "noEmit": true,
    "declaration": true,
    "declarationMap": true,
    "verbatimModuleSyntax": true,
    "moduleDetection": "force",
    "downlevelIteration": true,
    "allowJs": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "incremental": true,
    "tsBuildInfoFile": ".tsbuildinfo"
  },
  "include": [
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules",
    "src/tests"
  ]
}

现在我们将创建next.json配置:

json 复制代码
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "./base.json",
  "compilerOptions": {
    "paths": {
      "@/*": [
        "./*"
      ]
    },
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  }
}

然后我们需要创建express.json配置文件:

json 复制代码
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "ExpressJS Server",
  "extends": "./base.json",
  "ts-node": {
    "compilerOptions": {
      "module": "commonjs",
      "moduleResolution": "Node10"
    }
  },
  "compilerOptions": {
    "outDir": "./build",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "module": "ESNext"
  }
}

创建一个types.json配置文件,用于我们的共享类型包:

json 复制代码
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Shared Types",
  "extends": "./base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "declaration": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
  },
}

此外,我们需要添加最后的配置文件ui.json,这个文件将用于我们的共享UI包。

json 复制代码
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Shared UI",
  "extends": "./base.json",
  "compilerOptions": {
    "paths": {
      "@/*": [
        "./*"
      ]
    },
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  }
}

最后,创建utils.json配置:

json 复制代码
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Shared UI",
  "extends": "./base.json",
  "compilerOptions": {
    "paths": {
      "@/*": [
        "./*"
      ]
    },
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  }
}

这就是你的 tsconfig 包文件夹的样子:

恭喜,我们刚刚完成了typescript的配置。现在让我们进入激动人心的部分:创建我们的应用程序!


创建应用程序

步骤1:设置Next.js 15应用(web

创建并导航到apps/目录:

bash 复制代码
cd ../.. && mkdir apps && cd apps

使用pnpm创建一个新的Next.js应用:

lua 复制代码
pnpm create next-app@latest web --ts --app --turbopack --no-eslint --tailwind --src-dir --skip-install --import-alias @/*

此命令将在web/文件夹中创建一个新的Next.js应用,启用TypeScript,将Turbopack设置为默认打包器,并使用Tailwind CSS。

要将我们的tsconfig包集成到web应用中,我们需要更新默认的package.json

json 复制代码
...,
"dependencies": {
  "@monorepo/types": "workspace:*",
  "@monorepo/ui": "workspace:*",
  "@monorepo/utils": "workspace:*",
  "react": "19.0.0-rc-02c0e824-20241028",
  "react-dom": "19.0.0-rc-02c0e824-20241028",
  "next": "15.0.2"
},
"devDependencies": {
  "@monorepo/tsconfig": "workspace:*",
  "@types/node": "^20",
  "@types/react": "^18",
  "@types/react-dom": "^18",
  "postcss": "^8",
  "tailwindcss": "^3.4.1",
  "@biomejs/biome": "^1.7.2"
}
...,

现在,更新默认的tsconfig.json

json 复制代码
{
  "extends": "@monorepo/tsconfig/next.json",
  "compilerOptions": {
    "plugins": [
      {
        "name": "next"
      }
    ],
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts",
    "tailwind.config.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

添加biome.json以便我们可以在文件夹中激活它:

json 复制代码
{
  "extends": ["../../biome.json"]
}

你的Next.js应用现在已经设置好了!让我们继续使用Express.js设置后端应用。

步骤2:设置Express应用(server

导航回到apps/目录并创建一个Express应用:

bash 复制代码
cd .. && mkdir server && cd server && pnpm init

更新你的服务器的package.json以添加Express、其类型、cors、morgan和ts-node-dev:

json 复制代码
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "scripts": {
    "dev": "ts-node-dev --transpile-only src/server.ts"
  },
  "dependencies": {
    "@monorepo/types": "workspace:*",
    "express": "^4.21.1",
    "ts-node-dev": "^2.0.0",
    "cors": "2.8.5",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "@monorepo/tsconfig": "workspace:*",
    "@types/express": "^5.0.0",
    "@types/morgan": "^1.9.9",
    "@types/cors": "2.8.17"
  }
}

tsconfig.json添加到服务器:

json 复制代码
{
  "extends": "@monorepo/tsconfig/express.json",
  "include": [
    "src"
  ],
}

src/server.ts中创建一个基本的Express服务器:

json 复制代码
import cors from "cors";
import express from "express";
import morgan from "morgan";

const app = express();

app.use(morgan("tiny"));
app.use(express.json({ limit: "100mb" }));

app.use(
  cors({
    credentials: true,
    origin: ["http://localhost:3000"],
  }),
);

const port = process.env.PORT || 3001;

app.get("/", (_, res) => {
  res.send("Hello from Express!");
});

app.listen(port, () => {
  console.log(`Server is running on http://localhost:${port}`);
});

现在你已经设置了前端(Next.js)和后端(Express)应用!让我们继续创建两个应用都可以使用的共享包。


创建共享包

在这一部分,我们将创建三个共享包:一个用于UI组件(ui),一个用于TypeScript类型(types),一个用于实用函数(utils)。这些包将位于packages/目录中。

步骤1:创建utils

我们将创建的第一个包是用于实用函数(utils)。要设置它:

packages/内创建文件夹,初始化它:

bash 复制代码
cd ../.. && mkdir packages && cd packages && mkdir utils && cd utils && pnpm init && mkdir src && touch src/styles.ts 

更新package.json以添加脚本和导出:

json 复制代码
{
  "name": "@monorepo/utils",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "check-types": "tsc --noEmit",
    "build": "tsup",
    "lint": "biome lint ./src",
    "format": "biome format ./src "
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "clsx": "^2.1.1",
    "tailwind-merge": "^2.5.4"
  },
  "devDependencies": {
    "@monorepo/tsconfig": "workspace:*"
  },
  "exports": {
    ".": "./src",
    "./styles": "./src/styles.ts"
  }
}

添加biome.json

json 复制代码
{
    "extends": [
        "../../biome.json"
    ]
}

添加tsconfig.json

json 复制代码
{
  "extends": "@monorepo/tsconfig/utils.json",
  "include": [
    "**/*.ts",
  ],
  "exclude": [
    "node_modules"
  ],
}

我们将创建的第一个(也是唯一的)实用函数是cn,一个用于有条件地合并tailwind类的实用函数,它在ShadCN组件中被大量使用。我们需要添加以下依赖项:

sql 复制代码
pnpm add clsx tailwind-merge

src/style.ts中添加cn通用实用函数:

ts 复制代码
import clsx, { type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

步骤2:创建ui包(Tailwind CSS + ShadCN)

导航回到packages/目录:

bash 复制代码
cd .. && mkdir ui && cd ui && pnpm init

安装React以及Tailwind CSS(开发依赖)和ShadCN(我们将使用new york风格):

bash 复制代码
pnpm add -D @types/react @types/react-dom autoprefixer postcss react tailwindcss typescript
bash 复制代码
pnpm add shadcn @types/react tailwindcss-animate class-variance-authority clsx tailwind-merge @radix-ui/react-icons

按照与Next.js应用类似的步骤设置Tailwind CSS---初始化Tailwind CSS(npx tailwindcss init)并在tailwind.config.ts中配置它:

ts 复制代码
import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";

const config = {
  darkMode: ["class"],
  content: [
    "./pages/**/*.{ts,tsx}",
    "./components/**/*.{ts,tsx}",
    "./app/**/*.{ts,tsx}",
    "./src/**/*.{ts,tsx}",
    "../../packages/ui/src/**/*.{ts,tsx}",
  ],
  prefix: "",
  theme: {
  	container: {
  		center: true,
  		padding: '2rem',
  		screens: {
  			'2xl': '1400px'
  		}
  	},
  	extend: {
  		colors: {
  			border: 'hsl(var(--border))',
  			input: 'hsl(var(--input))',
  			ring: 'hsl(var(--ring))',
  			background: 'hsl(var(--background))',
  			foreground: 'hsl(var(--foreground))',
  			primary: {
  				DEFAULT: 'hsl(var(--primary))',
  				foreground: 'hsl(var(--primary-foreground))'
  			},
  			secondary: {
  				DEFAULT: 'hsl(var(--secondary))',
  				foreground: 'hsl(var(--secondary-foreground))'
  			},
  			destructive: {
  				DEFAULT: 'hsl(var(--destructive))',
  				foreground: 'hsl(var(--destructive-foreground))'
  			},
  			muted: {
  				DEFAULT: 'hsl(var(--muted))',
  				foreground: 'hsl(var(--muted-foreground))'
  			},
  			accent: {
  				DEFAULT: 'hsl(var(--accent))',
  				foreground: 'hsl(var(--accent-foreground))'
  			},
  			popover: {
  				DEFAULT: 'hsl(var(--popover))',
  				foreground: 'hsl(var(--popover-foreground))'
  			},
  			card: {
  				DEFAULT: 'hsl(var(--card))',
  				foreground: 'hsl(var(--card-foreground))'
  			}
  		},
  		borderRadius: {
  			lg: '`var(--radius)`',
  			md: '`calc(var(--radius) - 2px)`',
  			sm: 'calc(var(--radius) - 4px)'
  		},
  		fontFamily: {
  			sans: ["var(--font-sans)", ...fontFamily.sans]
  		},
  		keyframes: {
  			'accordion-down': {
  				from: {
  					height: '0'
  				},
  				to: {
  					height: 'var(--radix-accordion-content-height)'
  				}
  			},
  			'accordion-up': {
  				from: {
  					height: 'var(--radix-accordion-content-height)'
  				},
  				to: {
  					height: '0'
  				}
  			},
  		},
  		animation: {
  			'accordion-down': 'accordion-down 0.2s ease-out',
  			'accordion-up': 'accordion-up 0.2s ease-out'
  		}
  	}
  },
  plugins: [tailwindcssAnimate],
} satisfies Config;

export default config;

我们还需要为Tailwind CSS 配置postcss.config.mjs

js 复制代码
/** @type {import('postcss-load-config').Config} */
const config = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

export default config;

由于我们也将在这个包上使用Biome,添加biome.json

json 复制代码
{
    "extends": [
        "../../biome.json"
    ]
}

更新package.json以添加tsconfigutils包和自定义脚本:

json 复制代码
{
  "name": "@monorepo/ui",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "check-types": "tsc --noEmit",
    "add-shadcn-component": "pnpm dlx shadcn@latest add",
    "build": "tsup",
    "lint": "biome lint ./src",
    "format": "biome format ./src "
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@monorepo/tsconfig": "workspace:*",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18",
    "autoprefixer": "^10.4.20",
    "postcss": "^8.4.47",
    "react": "19.0.0-rc-02c0e824-20241028",
    "tailwindcss": "^3.4.1"
  },
  "dependencies": {
    "@monorepo/utils": "workspace:^",
    "@radix-ui/react-accordion": "^1.2.1",
    "@radix-ui/react-icons": "^1.3.1",
    "@radix-ui/react-select": "^2.1.2",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "shadcn": "^2.1.3",
    "tailwind-merge": "^2.5.4",
    "tailwindcss-animate": "^1.0.7"
  },
  "exports": {
    "./globals.css": "./src/styles/globals.css",
    "./postcss.config": "./postcss.config.mjs",
    "./tailwind.config": "./tailwind.config.ts",
    "./components/*": "./src/*.tsx"
  }
}

创建一个tsconfig.json文件:

json 复制代码
{
  "extends": "@monorepo/tsconfig/ui.json",
  "include": [
    "**/*.ts",
    "**/*.tsx",
    "tailwind.config.ts",
  ],
  "exclude": [
    "node_modules"
  ],
}

src/styles/globals.css创建一个样式文件:

css 复制代码
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 222.2 47.4% 11.2%;

    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;

    --popover: 0 0% 100%;
    --popover-foreground: 222.2 47.4% 11.2%;

    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;

    --card: 0 0% 100%;
    --card-foreground: 222.2 47.4% 11.2%;

    --primary: 222.2 47.4% 11.2%;
    --primary-foreground: 210 40% 98%;

    --secondary: 210 40% 96.1%;
    --secondary-foreground: 222.2 47.4% 11.2%;

    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;

    --destructive: 0 100% 50%;
    --destructive-foreground: 210 40% 98%;

    --ring: 215 20.2% 65.1%;

    --radius: 0.5rem;
  }

  .dark {
    --background: 224 71% 4%;
    --foreground: 213 31% 91%;

    --muted: 223 47% 11%;
    --muted-foreground: 215.4 16.3% 56.9%;

    --accent: 216 34% 17%;
    --accent-foreground: 210 40% 98%;

    --popover: 224 71% 4%;
    --popover-foreground: 215 20.2% 65.1%;

    --border: 216 34% 17%;
    --input: 216 34% 17%;

    --card: 224 71% 4%;
    --card-foreground: 213 31% 91%;

    --primary: 210 40% 98%;
    --primary-foreground: 222.2 47.4% 1.2%;

    --secondary: 222.2 47.4% 11.2%;
    --secondary-foreground: 210 40% 98%;

    --destructive: 0 63% 31%;
    --destructive-foreground: 210 40% 98%;

    --ring: 216 34% 17%;

    --radius: 0.5rem;
  }
}

@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
    font-feature-settings: "rlig" 1, "calt" 1;
  }
}

ShadCN需要你创建一个components.json启用CLI使用):

json 复制代码
{
    "$schema": "https://ui.shadcn.com/schema.json",
    "style": "new-york",
    "rsc": true,
    "tsx": true,
    "tailwind": {
        "config": "tailwind.config.ts",
        "css": "src/styles/globals.css",
        "baseColor": "slate",
        "cssVariables": true
    },
    "aliases": {
        "components": "src/",
        "ui": "src/",
        "utils": "@monorepo/utils/styles"
    }
}

现在你可以开始在这个包中添加可重用的UI组件了!例如,要导入ShadCN按钮组件,只需在工作区根目录运行以下命令:

csharp 复制代码
pnpm add-shadcn-component card

你可以在这里找到每个ShadCN组件,在这里找到基于它创建的其他组件。现在我们准备设置我们的共享类型包并集成我们已设置的所有内容!

步骤3:创建types

types包将包含两个应用都可以使用的共享TypeScript类型。要创建它:

导航回到packages/,创建文件夹并初始化它:

bash 复制代码
cd .. && mkdir types && cd types && pnpm init

创建biome.json文件:

json 复制代码
{
    "extends": [
        "../../biome.json"
    ]
}

创建tsconfig.json文件:

json 复制代码
{
  "extends": "@monorepo/tsconfig/types.json",
  "include": [
      "**/*.ts",
  ],
  "exclude": [
    "node_modules"
  ],
}

我们将创建的第一个类型是一个简单的api客户端,这样我们可以在serverweb之间共享类型。创建src/文件夹,在其中创建api/文件夹。然后创建simple-api-client.ts

ts 复制代码
export interface GetTestResponse {
  message: string;
}

export type GetTest = () => Promise<GetTestResponse>;

export interface SimpleApiClient {
  getTest: GetTest;
}

更新package.json以添加导出、脚本和devDependencies:

ts 复制代码
{
  "name": "@monorepo/types",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "tsc",
    "lint": "biome lint ./src",
    "check-types": "tsc --noEmit"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@monorepo/tsconfig": "workspace:*"
  },
  "exports": {
    ".": "./src/index.ts"
  }
}

现在,在src/api文件夹创建一个index.ts,并从simple-api-client.ts导出所有内容(你将使用其他文件复制它,以拥有单一导入源):

javascript 复制代码
export * from "./simple-api-client";

最后,在src文件夹创建一个 index.ts 并从 api 导出所有内容:

我们的共享类型包已经全部设置好了!你的代码库应该看起来像这样:

现在,让我们继续我们教程的最后部分:集成所有内容并运行开发环境


在本地运行你的Monorepo

现在一切都设置好了,让我们在本地运行两个应用程序!

步骤1:安装所有依赖项

要一次性安装工作区中的所有依赖项(记得切换回根目录):

复制代码
pnpm install

这个命令安装两个应用(webserver)以及所有共享包(uitypes等)所需的所有依赖项。

步骤2:同时运行两个应用

arduino 复制代码
pnpm turbo run dev

这个命令同时启动你的前端(Next.js )在端口3000和后端(Express)在端口3001!


Web和Server集成

为了在我们的应用和包之间创建一个简单的集成,我们将开发一个组件,该组件将使用我们在本教程前面创建的共享类型从服务器获取数据。但是,在此之前,让我们更新我们的Tailwind CSS 文件和全局样式 ,使用我们在UI包中定义的那些。将tailwind.config.ts的内容替换为以下内容:

javascript 复制代码
export * from "@monorepo/ui/tailwind.config";

现在将postcss.config.mjs内容替换为:

javascript 复制代码
export { default } from "@monorepo/ui/postcss.config";

在我们的根布局(src/app/layout.tsx)中,更新globals.css导入以使用我们在UI包中创建的那个:

tsx 复制代码
import "@monorepo/ui/globals.css";
import "./style.css";
import type { Metadata } from "next";
import localFont from "next/font/local";

const geistSans = localFont({
  src: "./fonts/GeistVF.woff",
  variable: "--font-geist-sans",
  weight: "100 900",
});
const geistMono = localFont({
  src: "./fonts/GeistMonoVF.woff",
  variable: "--font-geist-mono",
  weight: "100 900",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased dark`}>{children}</body>
    </html>
  );
}

这样做是为了我们可以从共享UI包控制我们的应用UI样式和配置,这样如果我们创建另一个web应用(例如管理仪表板),我们就有一致的样式!

要开始开发我们的应用程序,我们将按照以下规则组织web文件夹结构:只在一个页面中使用的组件应该在app目录中与页面文件夹相同级别的components文件夹内。应用共享组件应该在src/components文件夹中。

所以,让我们在app目录中创建一个components文件夹src/app/components(我们将创建一个只在第一个页面使用的组件),并创建一个名为get-test.tsx的文件,内容如下:

tsx 复制代码
"use client";

import type { GetTestResponse } from "@monorepo/types";
import { Card, CardContent, CardHeader } from "@monorepo/ui/components/card";
import { cn } from "@monorepo/utils/styles";
import { useEffect, useState } from "react";

const GetTest = () => {
  const [test, setTest] = useState<string>("");

  useEffect(() => {
    const fetchTest = async () => {
      const response = await fetch("http://localhost:3001/test");
      const data: GetTestResponse = await response.json();
      setTimeout(() => {
        setTest(data.message);
      }, 3000);
    };
    fetchTest();
  }, []);

  return (
    <div>
      <Card>
        <CardHeader>
          <h1 className={cn("text-xl text-yellow-500", test !== "" && "text-green-500")}>Get Test</h1>
        </CardHeader>
        <CardContent>
          <p>{test}</p>
        </CardContent>
      </Card>
    </div>
  );
};

export default GetTest;

看一下这个文件,我们有一个简单的fetch到我们的服务器,使用我们在共享类型中定义的类型化响应,这使我们可以轻松地使用响应。服务器路由将很快定义。首先,让我们通过在page.tsx中导入组件来完成web部分:

tsx 复制代码
import GetTest from "./components/get-test";

export default function Home() {
  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <GetTest />
    </div>
  );
}

web设置完成后,转到server应用并使用我们将使用的路由更新server.ts。在app.listen之前添加以下路由:

ts 复制代码
app.get("/test", (_, res) => {
  const testJson: GetTestResponse = {
    message: "Hello from Express API!",
  };
  res.json(testJson);
});

最后,一切准备就绪,你可以使用以下命令运行整个应用程序(记得切换回根目录):

你将能够看到在页面上渲染的以下组件。

一旦组件被渲染,我们在useEffect中fetch服务器并设置状态以进行渲染(setTimeout不是必需的,它只是为了可视化状态改变),然后你将看到以下内容。

结论

恭喜!你已成功设置了一个可扩展的 monorepo,其中包含两个应用程序---一个使用 Next.js 构建的前端,使用 Tailwind CSS 样式,由 ShadCN 的可重用组件增强,以及一个 Express 后端---所有这些都使用 pnpm 工作区高效管理,以及由 Turbopack 提供的超快构建和几乎即时的 Biome 检查!

以下是你今天完成的工作:

  • 在一个代码库中创建了两个独立的应用程序。
  • 设置了四个包,其中三个包含可重用代码,一个用于 typescript 配置。
  • 通过 pnpm 工作区使用高效的依赖管理实践将这些包链接到应用程序之间。

我们已经到达教程的结尾,现在你可以自由发挥想象力,使用最佳的 monorepo 架构创建任何你想要的东西。

相关推荐
AlienZHOU3 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc6 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师7 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen8 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒8 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow8 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8188 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800788 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明8 小时前
第 6 章 组件进阶
前端·vue.js
江华森8 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端