从 v6 升级到 v7

AdonisJS v7 是在 v6 历时两年之后的一个重大版本发布。本指南涵盖你为使现有 v6 应用升级而必须做出的更改。

我们已尽力将破坏性变更的范围保持在较小水平。然而,仍然存在一些破坏性变更,且某些更新是必需的。在基础层面上:

  • AdonisJS v7 需要 Node.js 24
  • 兼容 TypeScript 5.9/6.0 与 ESLint 10
  • Vite 集成已更新以适配 Vite 7

有用的链接

  • v6 文档 —— 如果你在升级期间需要参考旧版 API。
  • 报告升级问题 —— 遇到了意想不到的情况?在这里发帖,我们会提供帮助。

升级到 Node.js 24

AdonisJS v7 需要 Node.js 24 或更高版本。较旧的 Node.js 版本不再受支持。在继续本指南的其余部分之前,请确保更新你的本地开发环境、CI 流水线以及生产服务器。

node -v

使用编码代理(coding agent)进行升级

使用以下提示词与你的编码代理(Cursor、Claude Code、Copilot 等)配合,以处理升级中机械性的部分。请将它所做的更改与上面列出的破坏性变更进行核对。

升级所有包

将你项目中的每个 @adonisjs/* 包更新到其最新版本。你还必须将 @vinejs/vine、edge.js 和 Inertia 依赖升级到最新版本。

以下是一个跨平台脚本,你可以运行它来自动查找项目 package.json 文件中 AdonisJS 特定的依赖并一次性更新它们。

npm i $(node -e "const pkg = require('./package.json'); const deps = {...pkg.dependencies, ...pkg.devDependencies}; console.log(Object.keys(deps).filter(k => k.startsWith('@adonisjs/') || k === '@vinejs/vine' || k === 'edge.js' || k === '@japa/plugin-adonisjs' || k === 'vite' || k === 'argon2').map(k => k + '@latest').join(' '))") --force

替换 TypeScript JIT 编译器

我们已将 ts-node(和 ts-node-maintained)替换为 @poppinss/ts-exec 作为 JIT 编译器。移除旧包并安装新包。

npm uninstall ts-node ts-node-maintained @swc/core
npm install -D @poppinss/ts-exec

然后更新你 ace.js 文件中的导入。

ace.js
import 'ts-node-maintained/register/esm'
import '@poppinss/ts-exec'

将 Youch 安装为项目依赖

Youch 不再打包在 @adonisjs/ace 和 @adonisjs/http-server 内部。它已从头重写,但这不会影响你的应用代码,因为 Youch 是框架在内部消费的。你只需将其作为开发依赖安装即可。

npm install -D youch

在 adonisrc.ts 中配置钩子

v7 在 adonisrc.ts 中引入了一个新的钩子系统。你至少必须添加 indexEntities 钩子。根据你的技术栈,你还需要为 Inertia、Tuyau、Bouncer 和 Vite 添加额外的钩子。

如果你的应用使用了 Tuyau,请确保安装 @tuyau/core 包。

npm install @tuyau/core

以下示例展示了一个完整的钩子配置。只包含与你的技术栈相关的钩子。

adonisrc.ts
import { indexEntities } from '@adonisjs/core'
import { indexPages } from '@adonisjs/inertia'
import { defineConfig } from '@adonisjs/core/app'
import { indexPolicies } from '@adonisjs/bouncer'
import { generateRegistry } from '@tuyau/core/hooks'

export default defineConfig({
  hooks: {
    init: [
      // Always needed
      indexEntities(),

      // If using Inertia (adjust framework to match yours)
      indexPages({ framework: 'react' }),
      generateRegistry(),
      indexEntities({
        transformers: { enabled: true, withSharedProps: true },
      }),

      // If using Bouncer
      indexPolicies(),
    ],
    buildStarting: [
      // If using Vite
      () => import('@adonisjs/vite/build_hook'),
    ],
  },
})

汇编器钩子已重命名

汇编器(assembler)钩子的名称已更改。如果你之前使用的是 onBuildStarting 钩子(最常见的一个,用于 Vite),请将其更新为 buildStarting。

adonisrc.ts
{
  hooks: {
    onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
    buildStarting: [() => import('@adonisjs/vite/build_hook')],
  }
}

完整已重命名的钩子列表如下。

- onSourceFileChanged
+ fileChanged

- onDevServerStarted
+ devServerStarted

- onBuildCompleted
+ buildFinished

- onBuildStarting
+ buildStarting

+ fileAdded
+ fileRemoved
+ devServerStarting
+ testsStarting
+ testsFinished

更新测试 glob 模式

我们用 Node.js 内置的 glob 辅助函数替换了 glob 包。这需要在 adonisrc.ts 内的测试文件模式上做一个小的语法变更。

adonisrc.ts
tests: {
  suites: [
    {
      files: ['tests/unit/**/*.spec(.ts|.js)'],
      files: ['tests/unit/**/*.spec.{ts,js}'],
    },
  ],
  forceExit: false,
},

移除 assetsBundler 属性

adonisrc.ts 中的 assetsBundler 属性已不再使用。移除它以解决升级后你会看到的 TypeScript 错误。

加密配置变更

来自 config/app.ts 的 appKey 导出不再用于加密。相反,你必须创建一个专用的 config/encryption.ts 文件。APP_KEY 环境变量仍然在使用。

config/app.ts
export const appKey = env.get('APP_KEY')

v6 应用必须使用 legacy 驱动来继续解密现有数据。了解更多关于新的 加密驱动 的信息

config/encryption.ts
import env from '#start/env'
import { defineConfig, drivers } from '@adonisjs/core/encryption'

export default defineConfig({
  default: 'legacy',
  list: {
    legacy: drivers.legacy({
      keys: [env.get('APP_KEY')],
    }),
  },
})

从容器中解析 encryption 绑定现在返回的是 EncryptionManager 实例,而非 Encryption 类。这是因为重写后的加密包支持多种算法,并使用管理器来在它们之间切换。

const encryption = await app.container.make('encryption')

// In v6
encryption instanceof Encryption

// In v7
encryption instanceof EncryptionManager

如果你之前是从容器中解析 Encryption 类并将其作为依赖传入,请通过直接解析该类构造函数来修复此问题。

import { Encryption } from '@adonisjs/core/encryption'
const encryption = await app.container.make(Encryption)

encryption instanceof Encryption // true

router.makeUrl 已废弃,改用 URL 构建器

router.makeUrl 和 router.makeSignedUrl 方法已被废弃。请改用新的类型安全 urlFor 辅助函数。

import router from '@adonisjs/core/services/router'
router.makeUrl('posts.show', { id: 1 })

import { urlFor } from '@adonisjs/core/services/url_builder'
urlFor('posts.show', { id: 1 })

在 Edge 模板内部,route 辅助函数已废弃,改用 urlFor。

route('posts.show', { id: 1 })
urlFor('posts.show', { id: 1 })

已移除的辅助函数

以下辅助函数已从 @adonisjs/core/helpers 中移除。每一个都有直接对应的替代方案。

已移除的辅助函数替代方案
getDirnameimport.meta.dirname
getFilenameimport.meta.filename
slashstringHelpers.toUnixSlash
joinToURLnew URL()
cuid / isCuid改用 UUID
parseImports直接使用 parse-imports 包
import { getDirname, getFilename } from '@adonisjs/core/helpers'
getDirname()
getFilename()

import.meta.dirname
import.meta.filename
import { slash } from '@adonisjs/core/helpers'
slash('foo\\bar') // foo/bar

import stringHelpers from '@adonisjs/core/helpers/string'
stringHelpers.toUnixSlash('foo\\bar') // foo/bar

Request 和 Response 类已重命名

HTTP 包中的 Request 和 Response 类已重命名为 HttpRequest 和 HttpResponse。这避免了与全局可用的平台原生 Request 和 Response 类产生冲突。

大多数项目不会受影响,因为绝大多数代码库是与 HttpContext 对象交互,而不是直接导入这些类。然而,如果你扩展了这些类、使用模块增强来添加自定义属性、或者在它们上面注册了宏(macro),你将需要更新你的代码。

import { Request } from '@adonisjs/core/http'
import { HttpRequest } from '@adonisjs/core/http'

declare module '@adonisjs/core/http' {
  interface Request {
    someMethod(): void
  }

  interface HttpRequest {
    someMethod(): void
  }
}

Request.macro('someMethod', () => {})
HttpRequest.macro('someMethod', () => {})

Flash 消息的 errors 键已移除

已从 flash 消息存储中移除了已废弃的 errors 键。

验证错误一直以来都可通过 inputErrorsBag 键获取。errors 键是一个重复的键,会不必要地增大会话的负载大小。

如果你的模板或前端代码从 errors 读取数据,请将它们更新为改用 inputErrorsBag。

{{ flashMessages.get('errors.email') }}
{{ flashMessages.get('inputErrorsBag.email') }}

多部分(multipart)文件与字段在 request.all() 中合并

调用 request.all() 方法现在会返回一个合并了请求字段与 multipart 文件的对象。之前,它只返回字段。

request.allFiles()、request.file(name) 和 request.files(name) 方法继续像以前一样工作。

控制器自动生成路由名称

使用控制器的路由现在会自动获得一个生成的名称。这可能导致冲突:

  • 如果你有两条指向同一个控制器方法的路由,其中只有一条具有显式名称。
  • 未命名路由的自动生成名称会与另一条冲突,并在启动时抛出重复路由错误。
  • 你会在启动应用时立即捕获到这个问题。

针对 JSON API 请求跳过的状态页面

由 全局异常处理器 渲染的状态页面,在请求的 Accept 头要求 JSON 响应时不再返回。API 客户端现在会收到 JSON 错误响应,而非渲染的 HTML 页面。这原本是一个 bug 修复,但如果你之前的 API 消费者收到的是 HTML 错误页面,这会改变其行为。

VineJS 中移除了 BaseModifiers

在最新版本的 VineJS 中,已移除 BaseModifiers 类。在大多数情况下,这不会影响你的应用。然而,如果你为了某个自定义用例而扩展或直接使用 BaseModifiers,你将需要调整你的实现。详情请参阅 VineJS v4 发布说明 。

应用关闭钩子以逆序运行

关闭(shutdown)钩子现在以逆序执行(最后注册,最先执行)。这是一个 bug 修复,与清理逻辑的预期行为一致,但如果你依赖之前(不正确)的顺序,可能会影响你的应用。

Inertia 集成重写

Inertia 集成在 v7 中进行了重大重构。目标是为 render 方法和页面 props 带来端到端的类型安全,在计算 props 时为 Transformers 添加一流的支持,并与 Inertia 生态系统中的上游变更保持一致。

类型安全的 render 方法

inertia.render 方法现在是类型安全的。这可能会在你的应用中暴露出之前被允许的不合法或不完整数据的 TypeScript 错误。

配置变更

已对 config/inertia.ts 文件进行以下更改。

config/inertia.ts
export default defineConfig({
  entrypoint: 'inertia/app/app.tsx',

  history: {
    encrypt: true,
  },
  encryptHistory: true,

  sharedData: {
    // ...
  },
  // Replaced by inertia_middleware (see below)
})
  • 已移除 entrypoint 配置选项。此配置选项在任何地方都未被使用。
  • history.encrypt 选项已重命名为顶层的 encryptHistory 属性。
  • 已移除 sharedData 属性,改用 Inertia 中间件(见下文)。

文件结构变更

Inertia 入口点和 SSR 文件已从 app 子目录中移出,现在直接位于 inertia 根目录下。

- inertia/app/app.tsx
+ inertia/app.tsx

- inertia/app/ssr.tsx
+ inertia/ssr.tsx

确切的文件扩展名取决于你的框架。例如,Vue 应用将使用 inertia/app.ts 和 inertia/ssr.ts。

共享数据移至中间件

config/inertia.ts 中的 sharedData 属性已被移除。你必须创建一个 Inertia 中间件来定义共享数据,并将其注册为 kernel 文件中的服务器中间件。

node ace make:middleware inertia_middleware
start/kernel.ts
server.use([
  () => import('#middleware/inertia_middleware'),
])

share 方法接收 HttpContext,如果在请求到达路由处理器之前就发送了响应(例如,在 404 期间),该上下文可能未被完全填充。请谨慎地守卫你的属性访问。

以下是默认的中间件文件。由于你是从 v6 升级,你的应用中不会有 UserTransformer。因此请随意移除这个导入,并按照代码库其他部分的方式序列化用户。

app/middleware/inertia_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'
import UserTransformer from '#transformers/user_transformer'
import BaseInertiaMiddleware from '@adonisjs/inertia/inertia_middleware'

export default class InertiaMiddleware extends BaseInertiaMiddleware {
  share(ctx: HttpContext) {
    /**
     * The share method is called every time an Inertia page is rendered. In
     * certain cases, a page may get rendered before the session middleware
     * or the auth middleware are executed. For example: During a 404 request.
     *
     * In that case, we must always assume that HttpContext is not fully hydrated
     * with all the properties.
     */
    const { session, auth } = ctx as Partial<HttpContext>

    /**
     * Data shared with all Inertia pages. Make sure you are using
     * transformers for rich data-types like Models.
     */
    return {
      errors: ctx.inertia.always(this.getValidationErrors(ctx)),
      flash: ctx.inertia.always({
        error: session?.flashMessages.get('error'),
        success: session?.flashMessages.get('success'),
      }),
      user: ctx.inertia.always(auth?.user ? UserTransformer.transform(auth.user) : undefined),
    }
  }

  async handle(ctx: HttpContext, next: NextFn) {
    await this.init(ctx)

    const output = await next()
    this.dispose(ctx)

    return output
  }
}

declare module '@adonisjs/inertia/types' {
  type MiddlewareSharedProps = InferSharedProps<InertiaMiddleware>
  export interface SharedProps extends MiddlewareSharedProps {}
}

添加 tsconfig.inertia.json

你必须创建一个新的 tsconfig.inertia.json 文件,以避免 Inertia 前端代码库与后端代码库之间的循环引用问题。

这种循环引用之所以发生,是因为代码生成(codegen)让 Inertia 应用引用后端代码,而后端代码又引用 Inertia 页面来推断 prop 类型。

在项目根目录创建该文件。

tsconfig.inertia.json
{
  "extends": "./inertia/tsconfig.json",
  "compilerOptions": {
    "rootDir": "./inertia",
    "composite": true
  },
  "include": ["./inertia/**/*.ts", "./inertia/**/*.tsx"]
}

然后将一个 references 条目添加到你的主 tsconfig.json 中。

tsconfig.json
{
  "extends": "@adonisjs/tsconfig/tsconfig.app.json",
  "compilerOptions": {
    "rootDir": "./",
    "jsx": "react",
    "outDir": "./build"
  },
  "references": [{ "path": "./tsconfig.inertia.json" }]
}

向 package.json 添加新的子路径导入

将以下条目添加到你的 package.json 的 imports 字段中。这些是对你项目已有的任何别名之外的补充。

package.json
{
  "imports": {
    "#generated/*": "./.adonisjs/server/*.js",
    "#transformers/*": "./app/transformers/*.js",
    "#database/*": "./database/*.js"
  }
}