从 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/vineedge.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.tsappKey 导出不再用于加密。相反,你必须创建一个专用的 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.makeUrlrouter.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

RequestResponse 类已重命名

HTTP 包中的 RequestResponse 类已重命名为 HttpRequestHttpResponse。这避免了与全局可用的平台原生 RequestResponse 类产生冲突。

大多数项目不会受影响,因为绝大多数代码库是与 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.tsinertia/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.jsonimports 字段中。这些是对你项目已有的任何别名之外的补充。

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