使用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": "[email protected]"
}

对于 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 架构创建任何你想要的东西。

相关推荐
cong_7 分钟前
🌟 Cursor 帮我 2.5 天搞了一个摸 🐟 岛
前端·后端·github
MyhEhud1 小时前
Kotlin 中 also 方法的用法和使用场景
前端·kotlin
小莫爱编程1 小时前
HTML,CSS,JavaScript
前端·css·html
陈大鱼头2 小时前
AI驱动的前端革命:10项颠覆性技术如何在LibreChat中融为一体
前端·ai 编程
Gazer_S2 小时前
【解析 ECharts 图表样式继承与自定义】
前端·信息可视化·echarts
剪刀石头布啊2 小时前
视觉格式化模型
前端·css
一 乐2 小时前
招聘信息|基于SprinBoot+vue的招聘信息管理系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·招聘系统
念九_ysl2 小时前
Vue3 + ECharts 数据可视化实战指南
前端·信息可视化·echarts
Gazer_S2 小时前
【Auto-Scroll-List 组件设计与实现分析】
前端·javascript·数据结构·vue.js
前端加油站3 小时前
前端开发人员必备的Mac应用
前端