From dcf9da141aece466b15777d1221c102546b79f7d Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 10 Aug 2026 15:09:11 +0530 Subject: [PATCH 1/2] chore: update dependencies and upgrade hypermedia to Vite 8 Bumps every starter kit to the latest releases of @adonisjs/core, @types/node, eslint, prettier, better-sqlite3 and turbo. The hypermedia kit also moves to @adonisjs/vite 6 and Vite 8, which renames the AdonisJS plugin "entrypoints" option to "entryPoints". TypeScript stays on 6.0.3, since @adonisjs/assembler declares "typescript: ^5.0.0 || ^6.0.0" as a peer dependency. --- api-monorepo/apps/backend/package.json | 12 ++++++------ api-monorepo/package.json | 4 ++-- api/package.json | 12 ++++++------ hypermedia/package.json | 16 ++++++++-------- hypermedia/vite.config.ts | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/api-monorepo/apps/backend/package.json b/api-monorepo/apps/backend/package.json index fbae901..8099a23 100644 --- a/api-monorepo/apps/backend/package.json +++ b/api-monorepo/apps/backend/package.json @@ -46,18 +46,18 @@ "@japa/plugin-adonisjs": "^5.2.0", "@japa/runner": "^5.3.0", "@poppinss/ts-exec": "^1.4.4", - "@types/luxon": "^3.7.1", - "@types/node": "~25.9.1", - "eslint": "^10.4.0", + "@types/luxon": "^3.7.4", + "@types/node": "~26.2.0", + "eslint": "^10.8.1", "hot-hook": "^1.0.0", "pino-pretty": "^13.1.3", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "typescript": "~6.0.3", "youch": "^4.1.1" }, "dependencies": { "@adonisjs/auth": "^10.1.0", - "@adonisjs/core": "^7.3.3", + "@adonisjs/core": "^7.4.0", "@adonisjs/cors": "^3.0.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/session": "^8.1.0", @@ -65,7 +65,7 @@ "@japa/api-client": "^3.2.1", "@tuyau/core": "^1.2.2", "@vinejs/vine": "^4.4.0", - "better-sqlite3": "^12.10.0", + "better-sqlite3": "^13.0.3", "luxon": "^3.7.2", "reflect-metadata": "^0.2.2" }, diff --git a/api-monorepo/package.json b/api-monorepo/package.json index 709afbb..26a78b1 100644 --- a/api-monorepo/package.json +++ b/api-monorepo/package.json @@ -21,8 +21,8 @@ }, "devDependencies": { "@adonisjs/prettier-config": "^1.5.0", - "prettier": "^3.8.3", - "turbo": "^2.9.16" + "prettier": "^3.9.6", + "turbo": "^2.10.9" }, "allowScripts": { "@swc/core": true, diff --git a/api/package.json b/api/package.json index 77cd854..708fa55 100644 --- a/api/package.json +++ b/api/package.json @@ -46,18 +46,18 @@ "@japa/plugin-adonisjs": "^5.2.0", "@japa/runner": "^5.3.0", "@poppinss/ts-exec": "^1.4.4", - "@types/luxon": "^3.7.1", - "@types/node": "~25.9.1", - "eslint": "^10.4.0", + "@types/luxon": "^3.7.4", + "@types/node": "~26.2.0", + "eslint": "^10.8.1", "hot-hook": "^1.0.0", "pino-pretty": "^13.1.3", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "typescript": "~6.0.3", "youch": "^4.1.1" }, "dependencies": { "@adonisjs/auth": "^10.1.0", - "@adonisjs/core": "^7.3.3", + "@adonisjs/core": "^7.4.0", "@adonisjs/cors": "^3.0.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/session": "^8.1.0", @@ -65,7 +65,7 @@ "@japa/api-client": "^3.2.1", "@tuyau/core": "^1.2.2", "@vinejs/vine": "^4.4.0", - "better-sqlite3": "^12.10.0", + "better-sqlite3": "^13.0.3", "luxon": "^3.7.2", "reflect-metadata": "^0.2.2" }, diff --git a/hypermedia/package.json b/hypermedia/package.json index 1c18ef4..e05d8a7 100644 --- a/hypermedia/package.json +++ b/hypermedia/package.json @@ -46,27 +46,27 @@ "@japa/runner": "^5.3.0", "@poppinss/ts-exec": "^1.4.4", "@types/alpinejs": "^3.13.11", - "@types/luxon": "^3.7.1", - "@types/node": "~25.9.1", + "@types/luxon": "^3.7.4", + "@types/node": "~26.2.0", "alpinejs": "^3.15.12", - "eslint": "^10.4.0", + "eslint": "^10.8.1", "hot-hook": "^1.0.0", "pino-pretty": "^13.1.3", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "typescript": "~6.0.3", - "vite": "^7.3.3", + "vite": "^8.2.1", "youch": "^4.1.1" }, "dependencies": { "@adonisjs/auth": "^10.1.0", - "@adonisjs/core": "^7.3.3", + "@adonisjs/core": "^7.4.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/session": "^8.1.0", "@adonisjs/shield": "^9.0.0", "@adonisjs/static": "^2.0.1", - "@adonisjs/vite": "^5.1.1", + "@adonisjs/vite": "^6.0.0", "@vinejs/vine": "^4.4.0", - "better-sqlite3": "^12.10.0", + "better-sqlite3": "^13.0.3", "edge.js": "^6.5.1", "luxon": "^3.7.2", "reflect-metadata": "^0.2.2" diff --git a/hypermedia/vite.config.ts b/hypermedia/vite.config.ts index 671111c..bd5d6b3 100644 --- a/hypermedia/vite.config.ts +++ b/hypermedia/vite.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ * Entrypoints of your application. Each entrypoint will * result in a separate bundle. */ - entrypoints: ['resources/css/app.css', 'resources/js/app.js'], + entryPoints: ['resources/css/app.css', 'resources/js/app.js'], /** * Paths to watch and reload the browser on file change From 390599f992eeb851711d34f0a68f1ddd78c3d3e0 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Mon, 10 Aug 2026 15:09:30 +0530 Subject: [PATCH 2/2] feat: migrate Inertia starter kits to @adonisjs/inertia 5 and Inertia 3 Upgrades both Inertia kits to @adonisjs/inertia 5, @inertiajs/{react,vue3} 3.6, @adonisjs/vite 6 and Vite 8, along with React 19.2, Vue 3.5.41 and the matching Vite framework plugins. Build setup: - Drop the "@adonisjs/inertia/vite" plugin, which no longer exists, and rename the AdonisJS plugin "entrypoints" option to "entryPoints". - Type the page glob as a component, since the v3 ComponentResolver no longer accepts a promise resolving to a module namespace. Flash messages: - Version 5 ships a first-class flash bag, so the flash messages move out of the middleware share method and into its new flash method. They are now sent as a top-level "flash" field, a sibling of "props", and the layouts read them from "usePage().flash". - inertia/types.ts bridges the inferred shared props and flash data into the Inertia client through the InertiaConfig interface. Templates: - Rename the "inertia" attribute on the title element to "data-inertia". Auth forms: The Form component now types its "errors" render prop from the route's declared request body, whereas version 4 left it permissive. Two forms rendered errors that the route types did not describe: - The login route had no validator, so its body was "{}". It now uses a loginValidator, which also means malformed input returns validation errors instead of falling through to verifyCredentials. - VineJS omits confirmation fields from the inferred input type, so "passwordConfirmation" is declared explicitly on the signup validator and destructured out before creating the user. axios is added as a direct dependency: @inertiajs/core declares it as an optional peer dependency, so npm skips it, but it remains the default HTTP client and is imported at runtime. SSR stays disabled in both kits, so serverEntryPoints is omitted. --- inertia-react/.adonisjs/client/data.d.ts | 3 +- .../.adonisjs/client/registry/schema.d.ts | 6 ++-- .../app/controllers/new_account_controller.ts | 2 +- .../app/controllers/session_controller.ts | 3 +- .../app/middleware/inertia_middleware.ts | 31 ++++++++++------- inertia-react/app/validators/user.ts | 15 ++++++++- inertia-react/inertia/app.tsx | 6 ++-- inertia-react/inertia/layouts/default.tsx | 10 +++--- inertia-react/inertia/ssr.tsx | 6 ++-- inertia-react/inertia/types.ts | 12 +++++++ inertia-react/package.json | 33 ++++++++++--------- .../resources/views/inertia_layout.edge | 2 +- inertia-react/vite.config.ts | 4 +-- inertia-vue/.adonisjs/client/data.d.ts | 3 +- .../.adonisjs/client/registry/schema.d.ts | 6 ++-- .../app/controllers/new_account_controller.ts | 2 +- .../app/controllers/session_controller.ts | 3 +- .../app/middleware/inertia_middleware.ts | 31 ++++++++++------- inertia-vue/app/validators/user.ts | 11 +++++++ inertia-vue/inertia/layouts/default.vue | 2 +- inertia-vue/inertia/types.ts | 13 ++++++++ inertia-vue/package.json | 31 ++++++++--------- .../resources/views/inertia_layout.edge | 2 +- inertia-vue/vite.config.ts | 4 +-- 24 files changed, 155 insertions(+), 86 deletions(-) create mode 100644 inertia-vue/inertia/types.ts diff --git a/inertia-react/.adonisjs/client/data.d.ts b/inertia-react/.adonisjs/client/data.d.ts index 3b8c719..9998cde 100644 --- a/inertia-react/.adonisjs/client/data.d.ts +++ b/inertia-react/.adonisjs/client/data.d.ts @@ -5,7 +5,7 @@ /// import type { InferData, InferVariants } from '@adonisjs/core/types/transformers' -import type { InferSharedProps } from '@adonisjs/inertia/types' +import type { InferSharedProps, InferFlashData } from '@adonisjs/inertia/types' import type UserTransformer from '#transformers/user_transformer' import type InertiaMiddleware from '#middleware/inertia_middleware' @@ -15,4 +15,5 @@ export namespace Data { export type Variants = InferVariants } export type SharedProps = InferSharedProps + export type FlashMessages = InferFlashData } diff --git a/inertia-react/.adonisjs/client/registry/schema.d.ts b/inertia-react/.adonisjs/client/registry/schema.d.ts index 1f3d623..bfeb118 100644 --- a/inertia-react/.adonisjs/client/registry/schema.d.ts +++ b/inertia-react/.adonisjs/client/registry/schema.d.ts @@ -59,12 +59,12 @@ export interface Registry { methods: ["POST"] pattern: '/login' types: { - body: {} + body: ExtractBody> paramsTuple: [] params: {} - query: {} + query: ExtractQuery> response: ExtractResponse>> - errorResponse: ExtractErrorResponse>> + errorResponse: ExtractErrorResponse>> | { status: 422; response: { errors: SimpleError[] } } } } 'session.destroy': { diff --git a/inertia-react/app/controllers/new_account_controller.ts b/inertia-react/app/controllers/new_account_controller.ts index 36b53fe..5e4f5d7 100644 --- a/inertia-react/app/controllers/new_account_controller.ts +++ b/inertia-react/app/controllers/new_account_controller.ts @@ -8,7 +8,7 @@ export default class NewAccountController { } async store({ request, response, auth }: HttpContext) { - const payload = await request.validateUsing(signupValidator) + const { passwordConfirmation, ...payload } = await request.validateUsing(signupValidator) const user = await User.create({ ...payload }) await auth.use('web').login(user) diff --git a/inertia-react/app/controllers/session_controller.ts b/inertia-react/app/controllers/session_controller.ts index 07b8569..838b6d8 100644 --- a/inertia-react/app/controllers/session_controller.ts +++ b/inertia-react/app/controllers/session_controller.ts @@ -1,4 +1,5 @@ import User from '#models/user' +import { loginValidator } from '#validators/user' import type { HttpContext } from '@adonisjs/core/http' export default class SessionController { @@ -7,7 +8,7 @@ export default class SessionController { } async store({ request, auth, response }: HttpContext) { - const { email, password } = request.all() + const { email, password } = await request.validateUsing(loginValidator) const user = await User.verifyCredentials(email, password) await auth.use('web').login(user) diff --git a/inertia-react/app/middleware/inertia_middleware.ts b/inertia-react/app/middleware/inertia_middleware.ts index 5ed6d82..6c9cdab 100644 --- a/inertia-react/app/middleware/inertia_middleware.ts +++ b/inertia-react/app/middleware/inertia_middleware.ts @@ -13,13 +13,7 @@ export default class InertiaMiddleware extends BaseInertiaMiddleware { * In that case, we must always assume that HttpContext is not fully hydrated * with all the properties */ - const { session, auth } = ctx as Partial - - /** - * Fetching the first error from the flash messages - */ - const error = session?.flashMessages.get('error') as string - const success = session?.flashMessages.get('success') as string + const { auth } = ctx as Partial /** * Data shared with all Inertia pages. Make sure you are using @@ -27,14 +21,29 @@ export default class InertiaMiddleware extends BaseInertiaMiddleware { */ return { errors: ctx.inertia.always(this.getValidationErrors(ctx)), - flash: ctx.inertia.always({ - error, - success, - }), user: ctx.inertia.always(auth?.user ? UserTransformer.transform(auth.user) : undefined), } } + /** + * The flash bag is sent to every Inertia page as a top-level "flash" field + * (a sibling of "props") and is read on the client using "usePage().flash". + * + * Just like the share method, the flash method may run before the session + * middleware, so HttpContext must be treated as partially hydrated. + */ + flash(ctx: HttpContext) { + const { session } = ctx as Partial + + /** + * Fetching the first error from the flash messages + */ + return { + error: session?.flashMessages.get('error') as string | undefined, + success: session?.flashMessages.get('success') as string | undefined, + } + } + async handle(ctx: HttpContext, next: NextFn) { await this.init(ctx) diff --git a/inertia-react/app/validators/user.ts b/inertia-react/app/validators/user.ts index 67548d8..45c2bd1 100644 --- a/inertia-react/app/validators/user.ts +++ b/inertia-react/app/validators/user.ts @@ -7,7 +7,11 @@ const email = () => vine.string().email().maxLength(254) const password = () => vine.string().minLength(8).maxLength(32) /** - * Validator to use when performing self-signup + * Validator to use when performing self-signup. + * + * The "passwordConfirmation" field is declared explicitly, so that it is part + * of the request body type shared with the frontend. Otherwise the signup form + * has no way to know about the errors reported for this field. */ export const signupValidator = vine.create({ fullName: vine.string().nullable(), @@ -15,4 +19,13 @@ export const signupValidator = vine.create({ password: password().confirmed({ confirmationField: 'passwordConfirmation', }), + passwordConfirmation: vine.string(), +}) + +/** + * Validator to use when logging in an existing user + */ +export const loginValidator = vine.create({ + email: email(), + password: vine.string(), }) diff --git a/inertia-react/inertia/app.tsx b/inertia-react/inertia/app.tsx index 15593dd..027021c 100644 --- a/inertia-react/inertia/app.tsx +++ b/inertia-react/inertia/app.tsx @@ -4,7 +4,7 @@ import { client } from './client' import Layout from '~/layouts/default' import { type Data } from '@generated/data' import { createRoot } from 'react-dom/client' -import { createInertiaApp } from '@inertiajs/react' +import { createInertiaApp, type ResolvedComponent } from '@inertiajs/react' import { TuyauProvider } from '@adonisjs/inertia/react' import { resolvePageComponent } from '@adonisjs/inertia/helpers' @@ -13,9 +13,9 @@ const appName = import.meta.env.VITE_APP_NAME || 'AdonisJS' createInertiaApp({ title: (title) => (title ? `${title} - ${appName}` : appName), resolve: (name) => { - return resolvePageComponent( + return resolvePageComponent( `./pages/${name}.tsx`, - import.meta.glob('./pages/**/*.tsx'), + import.meta.glob('./pages/**/*.tsx'), (page: ReactElement) => ) }, diff --git a/inertia-react/inertia/layouts/default.tsx b/inertia-react/inertia/layouts/default.tsx index 4de27e5..15efc90 100644 --- a/inertia-react/inertia/layouts/default.tsx +++ b/inertia-react/inertia/layouts/default.tsx @@ -5,17 +5,17 @@ import { type ReactElement, useEffect } from 'react' import { Form, Link } from '@adonisjs/inertia/react' export default function Layout({ children }: { children: ReactElement }) { - const { url } = usePage() + const { url, flash } = usePage() useEffect(() => { toast.dismiss() }, [url]) useEffect(() => { - if (children.props.flash.error) { - toast.error(children.props.flash.error) + if (flash.error) { + toast.error(flash.error) } - if (children.props.flash.success) { - toast.success(children.props.flash.success) + if (flash.success) { + toast.success(flash.success) } }) diff --git a/inertia-react/inertia/ssr.tsx b/inertia-react/inertia/ssr.tsx index 3e97872..77631c9 100644 --- a/inertia-react/inertia/ssr.tsx +++ b/inertia-react/inertia/ssr.tsx @@ -3,7 +3,7 @@ import { type ReactElement } from 'react' import Layout from '~/layouts/default' import { type Data } from '@generated/data' import ReactDOMServer from 'react-dom/server' -import { createInertiaApp } from '@inertiajs/react' +import { createInertiaApp, type ResolvedComponent } from '@inertiajs/react' import { TuyauProvider } from '@adonisjs/inertia/react' import { resolvePageComponent } from '@adonisjs/inertia/helpers' @@ -12,9 +12,9 @@ export default function render(page: any) { page, render: ReactDOMServer.renderToString, resolve: (name) => { - return resolvePageComponent( + return resolvePageComponent( `./pages/${name}.tsx`, - import.meta.glob('./pages/**/*.tsx', { eager: true }), + import.meta.glob('./pages/**/*.tsx', { eager: true }), (resolvedPage: ReactElement) => ) }, diff --git a/inertia-react/inertia/types.ts b/inertia-react/inertia/types.ts index ff40d8d..7653c0f 100644 --- a/inertia-react/inertia/types.ts +++ b/inertia-react/inertia/types.ts @@ -3,3 +3,15 @@ import { type PropsWithChildren } from 'react' import { type JSONDataTypes } from '@adonisjs/core/types/transformers' export type InertiaProps = PropsWithChildren + +/** + * Bridges the server side types into the Inertia client. "usePage().props" is + * typed from the Inertia middleware share method and "usePage().flash" from + * its flash method. + */ +declare module '@inertiajs/core' { + interface InertiaConfig { + sharedPageProps: Data.SharedProps + flashDataType: Data.FlashMessages + } +} diff --git a/inertia-react/package.json b/inertia-react/package.json index f7548ed..cdf4402 100644 --- a/inertia-react/package.json +++ b/inertia-react/package.json @@ -46,41 +46,42 @@ "@japa/plugin-adonisjs": "^5.2.0", "@japa/runner": "^5.3.0", "@poppinss/ts-exec": "^1.4.4", - "@types/luxon": "^3.7.1", - "@types/node": "~25.9.1", - "@types/react": "^19.2.15", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.2.0", - "eslint": "^10.4.0", + "@types/luxon": "^3.7.4", + "@types/node": "~26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "eslint": "^10.8.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", "hot-hook": "^1.0.0", "pino-pretty": "^13.1.3", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "typescript": "~6.0.3", - "vite": "^7.3.3", + "vite": "^8.2.1", "youch": "^4.1.1" }, "dependencies": { "@adonisjs/auth": "^10.1.0", - "@adonisjs/core": "^7.3.3", + "@adonisjs/core": "^7.4.0", "@adonisjs/cors": "^3.0.0", - "@adonisjs/inertia": "^4.2.0", + "@adonisjs/inertia": "^5.0.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/session": "^8.1.0", "@adonisjs/shield": "^9.0.0", "@adonisjs/static": "^2.0.1", - "@adonisjs/vite": "^5.1.1", - "@inertiajs/react": "^2.3.24", + "@adonisjs/vite": "^6.0.0", + "@inertiajs/react": "^3.6.1", "@tuyau/core": "^1.2.2", "@vinejs/vine": "^4.4.0", - "better-sqlite3": "^12.10.0", + "axios": "^1.19.0", + "better-sqlite3": "^13.0.3", "edge.js": "^6.5.1", "luxon": "^3.7.2", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", "reflect-metadata": "^0.2.2", - "sonner": "^2.0.7" + "sonner": "^2.0.8" }, "hotHook": { "boundaries": [ diff --git a/inertia-react/resources/views/inertia_layout.edge b/inertia-react/resources/views/inertia_layout.edge index 0b34b79..7f16a3f 100644 --- a/inertia-react/resources/views/inertia_layout.edge +++ b/inertia-react/resources/views/inertia_layout.edge @@ -3,7 +3,7 @@ - + <title data-inertia> AdonisJS diff --git a/inertia-react/vite.config.ts b/inertia-react/vite.config.ts index 522a656..11e85cc 100644 --- a/inertia-react/vite.config.ts +++ b/inertia-react/vite.config.ts @@ -1,13 +1,11 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import adonisjs from '@adonisjs/vite/client' -import inertia from '@adonisjs/inertia/vite' export default defineConfig({ plugins: [ react(), - inertia({ ssr: { enabled: false, entrypoint: 'inertia/ssr.tsx' } }), - adonisjs({ entrypoints: ['inertia/app.tsx'], reload: ['resources/views/**/*.edge'] }), + adonisjs({ entryPoints: ['inertia/app.tsx'], reload: ['resources/views/**/*.edge'] }), ], /** diff --git a/inertia-vue/.adonisjs/client/data.d.ts b/inertia-vue/.adonisjs/client/data.d.ts index 3b8c719..9998cde 100644 --- a/inertia-vue/.adonisjs/client/data.d.ts +++ b/inertia-vue/.adonisjs/client/data.d.ts @@ -5,7 +5,7 @@ /// import type { InferData, InferVariants } from '@adonisjs/core/types/transformers' -import type { InferSharedProps } from '@adonisjs/inertia/types' +import type { InferSharedProps, InferFlashData } from '@adonisjs/inertia/types' import type UserTransformer from '#transformers/user_transformer' import type InertiaMiddleware from '#middleware/inertia_middleware' @@ -15,4 +15,5 @@ export namespace Data { export type Variants = InferVariants } export type SharedProps = InferSharedProps + export type FlashMessages = InferFlashData } diff --git a/inertia-vue/.adonisjs/client/registry/schema.d.ts b/inertia-vue/.adonisjs/client/registry/schema.d.ts index 1f3d623..bfeb118 100644 --- a/inertia-vue/.adonisjs/client/registry/schema.d.ts +++ b/inertia-vue/.adonisjs/client/registry/schema.d.ts @@ -59,12 +59,12 @@ export interface Registry { methods: ["POST"] pattern: '/login' types: { - body: {} + body: ExtractBody> paramsTuple: [] params: {} - query: {} + query: ExtractQuery> response: ExtractResponse>> - errorResponse: ExtractErrorResponse>> + errorResponse: ExtractErrorResponse>> | { status: 422; response: { errors: SimpleError[] } } } } 'session.destroy': { diff --git a/inertia-vue/app/controllers/new_account_controller.ts b/inertia-vue/app/controllers/new_account_controller.ts index 36b53fe..5e4f5d7 100644 --- a/inertia-vue/app/controllers/new_account_controller.ts +++ b/inertia-vue/app/controllers/new_account_controller.ts @@ -8,7 +8,7 @@ export default class NewAccountController { } async store({ request, response, auth }: HttpContext) { - const payload = await request.validateUsing(signupValidator) + const { passwordConfirmation, ...payload } = await request.validateUsing(signupValidator) const user = await User.create({ ...payload }) await auth.use('web').login(user) diff --git a/inertia-vue/app/controllers/session_controller.ts b/inertia-vue/app/controllers/session_controller.ts index 07b8569..838b6d8 100644 --- a/inertia-vue/app/controllers/session_controller.ts +++ b/inertia-vue/app/controllers/session_controller.ts @@ -1,4 +1,5 @@ import User from '#models/user' +import { loginValidator } from '#validators/user' import type { HttpContext } from '@adonisjs/core/http' export default class SessionController { @@ -7,7 +8,7 @@ export default class SessionController { } async store({ request, auth, response }: HttpContext) { - const { email, password } = request.all() + const { email, password } = await request.validateUsing(loginValidator) const user = await User.verifyCredentials(email, password) await auth.use('web').login(user) diff --git a/inertia-vue/app/middleware/inertia_middleware.ts b/inertia-vue/app/middleware/inertia_middleware.ts index 5ed6d82..6c9cdab 100644 --- a/inertia-vue/app/middleware/inertia_middleware.ts +++ b/inertia-vue/app/middleware/inertia_middleware.ts @@ -13,13 +13,7 @@ export default class InertiaMiddleware extends BaseInertiaMiddleware { * In that case, we must always assume that HttpContext is not fully hydrated * with all the properties */ - const { session, auth } = ctx as Partial - - /** - * Fetching the first error from the flash messages - */ - const error = session?.flashMessages.get('error') as string - const success = session?.flashMessages.get('success') as string + const { auth } = ctx as Partial /** * Data shared with all Inertia pages. Make sure you are using @@ -27,14 +21,29 @@ export default class InertiaMiddleware extends BaseInertiaMiddleware { */ return { errors: ctx.inertia.always(this.getValidationErrors(ctx)), - flash: ctx.inertia.always({ - error, - success, - }), user: ctx.inertia.always(auth?.user ? UserTransformer.transform(auth.user) : undefined), } } + /** + * The flash bag is sent to every Inertia page as a top-level "flash" field + * (a sibling of "props") and is read on the client using "usePage().flash". + * + * Just like the share method, the flash method may run before the session + * middleware, so HttpContext must be treated as partially hydrated. + */ + flash(ctx: HttpContext) { + const { session } = ctx as Partial + + /** + * Fetching the first error from the flash messages + */ + return { + error: session?.flashMessages.get('error') as string | undefined, + success: session?.flashMessages.get('success') as string | undefined, + } + } + async handle(ctx: HttpContext, next: NextFn) { await this.init(ctx) diff --git a/inertia-vue/app/validators/user.ts b/inertia-vue/app/validators/user.ts index 7225d2a..de7595d 100644 --- a/inertia-vue/app/validators/user.ts +++ b/inertia-vue/app/validators/user.ts @@ -3,10 +3,21 @@ import vine from '@vinejs/vine' const email = () => vine.string().email().maxLength(254) const password = () => vine.string().minLength(8).maxLength(32) +/** + * "passwordConfirmation" is declared explicitly, so that it is part of the + * request body type shared with the frontend. Otherwise the signup form has + * no way to know about the errors reported for this field. + */ export const signupValidator = vine.create({ fullName: vine.string().nullable(), email: email().unique({ table: 'users', column: 'email' }), password: password().confirmed({ confirmationField: 'passwordConfirmation', }), + passwordConfirmation: vine.string(), +}) + +export const loginValidator = vine.create({ + email: email(), + password: vine.string(), }) diff --git a/inertia-vue/inertia/layouts/default.vue b/inertia-vue/inertia/layouts/default.vue index b0749b1..7d6c783 100644 --- a/inertia-vue/inertia/layouts/default.vue +++ b/inertia-vue/inertia/layouts/default.vue @@ -13,7 +13,7 @@ watch( ) watch( - () => page.props.flash, + () => page.flash, (flashMessages) => { if (flashMessages.error) { toast.error(flashMessages.error) diff --git a/inertia-vue/inertia/types.ts b/inertia-vue/inertia/types.ts new file mode 100644 index 0000000..66c7bfb --- /dev/null +++ b/inertia-vue/inertia/types.ts @@ -0,0 +1,13 @@ +import { type Data } from '@generated/data' + +/** + * Bridges the server side types into the Inertia client. "usePage().props" is + * typed from the Inertia middleware share method and "usePage().flash" from + * its flash method. + */ +declare module '@inertiajs/core' { + interface InertiaConfig { + sharedPageProps: Data.SharedProps + flashDataType: Data.FlashMessages + } +} diff --git a/inertia-vue/package.json b/inertia-vue/package.json index 87ad25d..df1d85f 100644 --- a/inertia-vue/package.json +++ b/inertia-vue/package.json @@ -46,38 +46,39 @@ "@japa/plugin-adonisjs": "^5.2.0", "@japa/runner": "^5.3.0", "@poppinss/ts-exec": "^1.4.4", - "@types/luxon": "^3.7.1", - "@types/node": "~25.9.1", - "@vitejs/plugin-vue": "^6.0.7", - "eslint": "^10.4.0", - "eslint-plugin-vue": "^10.9.1", + "@types/luxon": "^3.7.4", + "@types/node": "~26.2.0", + "@vitejs/plugin-vue": "^6.0.8", + "eslint": "^10.8.1", + "eslint-plugin-vue": "^10.10.0", "hot-hook": "^1.0.0", "pino-pretty": "^13.1.3", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "typescript": "~6.0.3", - "vite": "^7.3.3", - "vue-tsc": "^3.3.2", + "vite": "^8.2.1", + "vue-tsc": "^3.3.9", "youch": "^4.1.1" }, "dependencies": { "@adonisjs/auth": "^10.1.0", - "@adonisjs/core": "^7.3.3", + "@adonisjs/core": "^7.4.0", "@adonisjs/cors": "^3.0.0", - "@adonisjs/inertia": "^4.2.0", + "@adonisjs/inertia": "^5.0.0", "@adonisjs/lucid": "^22.4.2", "@adonisjs/session": "^8.1.0", "@adonisjs/shield": "^9.0.0", "@adonisjs/static": "^2.0.1", - "@adonisjs/vite": "^5.1.1", - "@inertiajs/vue3": "^2.3.24", + "@adonisjs/vite": "^6.0.0", + "@inertiajs/vue3": "^3.6.1", "@tuyau/core": "^1.2.2", "@vinejs/vine": "^4.4.0", - "@vue/server-renderer": "^3.5.35", - "better-sqlite3": "^12.10.0", + "@vue/server-renderer": "^3.5.41", + "axios": "^1.19.0", + "better-sqlite3": "^13.0.3", "edge.js": "^6.5.1", "luxon": "^3.7.2", "reflect-metadata": "^0.2.2", - "vue": "^3.5.35", + "vue": "^3.5.41", "vue-sonner": "^2.0.9" }, "hotHook": { diff --git a/inertia-vue/resources/views/inertia_layout.edge b/inertia-vue/resources/views/inertia_layout.edge index d13fc9a..df5cbf6 100644 --- a/inertia-vue/resources/views/inertia_layout.edge +++ b/inertia-vue/resources/views/inertia_layout.edge @@ -3,7 +3,7 @@ - + <title data-inertia> AdonisJS diff --git a/inertia-vue/vite.config.ts b/inertia-vue/vite.config.ts index 09f43f0..0c9148a 100644 --- a/inertia-vue/vite.config.ts +++ b/inertia-vue/vite.config.ts @@ -1,13 +1,11 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import adonisjs from '@adonisjs/vite/client' -import inertia from '@adonisjs/inertia/vite' export default defineConfig({ plugins: [ vue(), - inertia({ ssr: { enabled: false, entrypoint: 'inertia/ssr.ts' } }), - adonisjs({ entrypoints: ['inertia/app.ts'], reload: ['resources/views/**/*.edge'] }), + adonisjs({ entryPoints: ['inertia/app.ts'], reload: ['resources/views/**/*.edge'] }), ], resolve: {