在 Encore.ts 中使用 Turborepo 构建 Monorepo:共享包编译、prebuild 钩子与部署实践
2026/9/15 20:21:21 网站建设 项目流程

在 Encore.ts 中使用 Turborepo 构建 Monorepo:共享包编译、prebuild 钩子与部署实践

【免费下载链接】encoreThe infrastructure platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/encor/encore

Encore 是面向现代云原生后端的基础设施平台,其 TypeScript 运行时(Encore.ts)在启动时会解析整个应用的结构。当应用位于 Turborepo monorepo 中并依赖需要先编译的共享包时,依赖的构建顺序就成为了关键问题。本指南基于 docs/ts/develop/monorepo/turborepo.md 展开,介绍如何在 Turborepo monorepo 中接入 Encore 应用,通过 Turborepo 的构建管线解决本地开发时的依赖构建顺序,并利用 Encore 的prebuild钩子保证云端部署与 Docker 导出时依赖同样被正确编译。读完本文,你将掌握完整的 monorepo 目录规划、turbo.json任务依赖编排、encore.app钩子配置以及本地与部署两条运行路径的完整实战方案。

为什么 Encore 应用需要先构建共享包

Turborepo 是 JavaScript/TypeScript monorepo 的构建系统(build system),它通过任务缓存与依赖图调度来加速 monorepo 中的构建流程。在一个典型的 Turborepo monorepo 中,packages/目录下往往存放着工具函数库、共享类型定义等需要先由 TypeScript 编译成 JavaScript 的包,而apps/下的业务应用则在运行时 import 这些包。

对于 Encore 来说,这一点尤为关键:Encore 在启动时会解析(parse)你的整个应用,包括所有被 import 的源码。如果共享包只有.ts源码而尚未编译产出dist/目录,Encore 解析应用时就无法找到这些依赖的编译产物,导致启动或构建失败。因此在 monorepo 场景下,共享包必须先构建、后运行。

围绕这一核心约束,需要解决两个场景的依赖构建顺序:

  • 本地开发(Local development):在运行encore run之前,先用 Turborepo 构建依赖包;
  • 部署(Deployment):在通过 Encore Cloud 部署或导出 Docker 镜像时,自动执行依赖构建——这一步由 Encore 的prebuild钩子完成。

项目结构:一个典型的 Turborepo + Encore 布局

一个典型的混合布局如下,apps/backend是 Encore 应用,packages/shared是需要编译的共享库:

my-turborepo/ ├── apps/ │ └── backend/ # Encore application │ ├── encore.app │ ├── package.json │ ├── tsconfig.json │ └── article/ │ └── article.ts ├── packages/ │ └── shared/ # Shared library requiring build │ ├── package.json │ ├── tsconfig.json │ ├── src/ │ │ └── index.ts │ └── dist/ # Built output │ └── index.js ├── turbo.json ├── package.json └── package-lock.json

其中packages/shared/dist/tsc编译后的产物目录,它会被package.jsonmain/exports字段指向,是 Encore 应用实际 import 的目标。

配置 Turborepo 与共享包

根目录 package.json:npm workspaces 与 packageManager

在 monorepo 根目录的package.json中声明 npm workspaces,让npm install一次安装所有子包依赖,并声明 Turborepo 脚本:

{ "name": "my-turborepo", "private": true, "packageManager": "npm@10.0.0", "scripts": { "build": "turbo run build", "dev": "turbo run dev" }, "devDependencies": { "turbo": "^2.0.0", "typescript": "^5.0.0" }, "workspaces": [ "apps/*", "packages/*" ] }

packageManager字段是 Turborepo 的必需字段,用于确定包管理器的版本,应调整为你实际安装的 npm 版本(可通过npm --version查看)。workspaces中的apps/*packages/*将 Encore 应用与共享包都纳入统一依赖管理。

turbo.json:用任务依赖编排构建顺序

根目录turbo.json定义构建管线。关键在于@repo/backend#dev任务必须依赖@repo/shared#build,从而保证本地执行 dev 任务时共享包已被编译:

{ "$schema": "https://turbo.build/schema.json", "tasks": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, "@repo/backend#dev": { "dependsOn": ["@repo/shared#build"], "cache": false, "persistent": true }, "dev": { "cache": false, "persistent": true } } }

各任务字段含义:

  • build.dependsOn: ["^build"]:每个包的 build 任务依赖其依赖包(^表示仅依赖项,不含自身)的 build,自上而下递归构建;
  • build.outputs: ["dist/**"]:声明构建产物目录,供 Turborepo 远程/本地缓存使用;
  • @repo/backend#dev.dependsOn: ["@repo/shared#build"]:显式指定 backend 的 dev 任务在 shared 包构建完成后才执行;
  • cache: false+persistent: true:dev 这类长驻进程任务不缓存、视为常驻任务,避免 Turborepo 误以为 dev 已“完成”。

共享包:编译产物与类型声明

共享包需要把 TypeScript 编译为 JavaScript 并暴露产物。packages/shared/package.json

{ "name": "@repo/shared", "version": "1.0.0", "private": true, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "scripts": { "build": "tsc" }, "devDependencies": { "typescript": "^5.0.0" } }

packages/shared/tsconfig.json负责把src/编译到dist/并生成.d.ts类型声明:

{ "compilerOptions": { "outDir": "dist", "rootDir": "src", "moduleResolution": "bundler", "module": "ES2022", "target": "ES2022", "declaration": true }, "include": ["src"], "exclude": ["node_modules", "dist"] }

packages/shared/src/index.ts给出共享类型与工具函数的示例——这些内容在前端与后端之间共享:

// Types shared between frontend and backend export interface Article { slug: string; title: string; preview: string; } export interface CreateArticleRequest { title: string; content: string; } // Utility functions export function slugify(text: string): string { return text .toLowerCase() .trim() .replace(/[^\w\s-]/g, "") .replace(/\s+/g, "-"); } export function truncate(text: string, maxLength: number): string { if (text.length <= maxLength) return text; return text.slice(0, maxLength - 3) + "..."; }

配置 Encore 应用:encore.app 与 prebuild 钩子

Encore 应用需要两处关键配置:一是encore.app中使用prebuild钩子在部署期间构建依赖,二是package.json中声明对共享包的依赖。

创建 Encore 应用

apps/backend目录下执行encore app init --lang ts创建 Encore 应用。从源码看,encore app init会生成一个encore.app文件,其 lang 字段为"typescript"(参见 cli/cmd/encore/app/initialize.go 中tsEncoreAppData模板,以及 pkg/appfile/appfile.go 中LangTS = "typescript"的定义)。appfile包还表明,encore.app文件(pkg/appfile/appfile.go 中的Name = "encore.app")预期位于 Encore 应用的根目录(通常是 Git 仓库根目录)。

encore.app:prebuild 钩子

在生成的encore.app文件中添加prebuild钩子:

{ "id": "generated-id", "lang": "typescript", "build": { "hooks": { "prebuild": "npx turbo build --filter=@repo/backend^..." } } }

prebuild钩子会在通过 Encore Cloud 部署或使用 Encore CLI 导出 Docker 镜像时执行。过滤器@repo/backend^...指示 Turborepo 构建@repo/backend的全部依赖项,其中^排除了 backend 自身,只构建它的依赖(即@repo/shared)。

从 Encore 源码可以看到钩子机制的实现:encore.appbuild.hooks配置在 pkg/appfile/appfile.go 中定义为Hooks结构,包含PreBuildPostBuild两个钩子,每个Hook既可以写成字符串,也可以写成带commandenv的对象形式(Hook.UnmarshalJSON同时支持两种格式,pkg/appfile/appfile.go)。在导出 Docker 镜像的流程中,cli/daemon/export/export.go 会在解析应用之前检查hooks.PreBuild.IsSet(),若已配置则在应用根目录执行该钩子,随后才进入bld.Parse解析与编译阶段——这印证了文档中的核心主张:prebuild 钩子在 Encore 解析应用前执行,保证共享包产物就绪

package.json:声明依赖

apps/backend/package.json声明对共享包与encore.dev运行时的依赖:

{ "name": "@repo/backend", "version": "1.0.0", "type": "module", "scripts": { "dev": "encore run" }, "dependencies": { "@repo/shared": "*", "encore.dev": "latest" } }

encore.dev是 Encore.ts 的官方运行时依赖,提供encore.dev/api等模块;@repo/shared通过"*"直接引用 workspace 内的共享包。

在 Encore 服务中导入共享包

完成上述配置后,即可在 Encore 服务中直接 import 共享包的类型与函数。以apps/backend/article/article.ts为例,定义一个对外暴露的 POST API:

import { api } from "encore.dev/api"; import type { Article, CreateArticleRequest } from "@repo/shared"; import { slugify, truncate } from "@repo/shared"; export const create = api( { expose: true, method: "POST", path: "/article" }, async ({ title, content }: CreateArticleRequest): Promise<Article> => { return { slug: slugify(title), title: title, preview: truncate(content, 100), }; }, );

类型与函数分别通过import type与普通 import 引入,create端点接收CreateArticleRequest,复用共享包中的slugifytruncate生成文章 slug 与摘要。这里import type只引入类型(编译期擦除),而slugifytruncate是运行时值,需要确保共享包已构建出对应 JS 产物。

运行与部署

安装依赖

从 monorepo 根目录一次性安装所有 workspace(含 Turborepo 与各子包)的依赖:

$ npm install

本地开发:两条路径

本地开发需要先构建共享包,再运行encore run。可以从 monorepo 根目录手动执行:

$ npx turbo run build $ cd apps/backend && encore run

也可以直接使用 Turborepo 的dev任务,由turbo.json中的依赖关系自动处理构建顺序:

$ npx turbo run dev --filter=@repo/backend

turbo.json中的@repo/backend#dev任务配置保证了@repo/shared在 backend 的 dev 任务启动前完成构建。注意:prebuild钩子只在部署或 Docker 导出时执行,不会在本地开发时运行,因此本地仍需依赖 Turborepo 自身来完成依赖构建。

部署:prebuild 钩子自动构建

通过 Encore Cloud 部署或导出 Docker 镜像时,encore.app中的prebuild钩子会自动运行 Turborepo 构建管线(npx turbo build --filter=@repo/backend^...),确保共享包产物在应用解析前就绪,之后 Encore 的 CI/CD 再对应用进行解析与编译打包。

部署 monorepo 的根目录配置:将 monorepo 部署到 Encore Cloud 时,需要在应用设置中把根目录指向 Encore 应用所在目录:Settings > General > Root Directory(例如apps/backend),让 Encore Cloud 定位到encore.app所在位置。

关键要点

  • 本地开发:运行encore run之前先执行npx turbo run build,或使用npx turbo run dev --filter=@repo/backend让 Turborepo 自动处理依赖构建顺序;
  • prebuild 钩子encore.app中的prebuild钩子在部署(Encore Cloud)或 Docker 导出时运行,而非本地开发时——这是 Encore 在解析应用前完成依赖构建的关键机制(对应实现见 cli/daemon/export/export.go 与 pkg/appfile/appfile.go);
  • Turborepo 过滤器--filter=@repo/backend^...只构建 backend 的依赖(^排除包自身),避免重复构建 backend 本体;
  • 共享包必须编译:由于 Encore 启动时解析整个应用,依赖共享包必须预先编译出dist/产物,并确保main/exports/types指向正确的构建输出。

同类场景如需了解 Nx 集成方案,可参考 docs/ts/develop/monorepo/nx.md。若使用 Go 语言构建 Encore 应用,其 monorepo 集成方式与此不同,可参见 docs/go/cli/cli-reference.md 了解 CLI 相关能力。

【免费下载链接】encoreThe infrastructure platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/encor/encore

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询