Skip to content

Commit 13aa30a

Browse files
committed
feat: react tuyau adapter
1 parent e06b13a commit 13aa30a

File tree

7 files changed

+356
-5
lines changed

7 files changed

+356
-5
lines changed

package.json

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"./plugins/api_client": "./build/src/plugins/japa/api_client.js",
2020
"./vite": "./build/src/client/vite.js",
2121
"./helpers": "./build/src/client/helpers.js",
22+
"./react": "./build/src/client/react/index.js",
2223
"./factories": "./build/factories/main.js"
2324
},
2425
"scripts": {
@@ -43,12 +44,13 @@
4344
},
4445
"devDependencies": {
4546
"@adonisjs/assembler": "^8.0.0-next.14",
46-
"@adonisjs/core": "^7.0.0-next.8",
47+
"@adonisjs/core": "^7.0.0-next.10",
4748
"@adonisjs/eslint-config": "^3.0.0-next.4",
4849
"@adonisjs/prettier-config": "^1.4.5",
4950
"@adonisjs/session": "^8.0.0-next.0",
5051
"@adonisjs/tsconfig": "^2.0.0-next.3",
5152
"@adonisjs/vite": "^5.1.0-next.0",
53+
"@inertiajs/react": "^2.2.15",
5254
"@japa/api-client": "^3.1.0",
5355
"@japa/assert": "4.1.1",
5456
"@japa/expect-type": "^2.0.3",
@@ -58,7 +60,9 @@
5860
"@japa/snapshot": "^2.0.9",
5961
"@poppinss/ts-exec": "^1.4.1",
6062
"@release-it/conventional-changelog": "^10.0.1",
63+
"@tuyau/core": "^1.0.0-beta.1",
6164
"@types/node": "^24.8.1",
65+
"@types/react": "^19.2.2",
6266
"@types/supertest": "^6.0.3",
6367
"c8": "^10.1.3",
6468
"copyfiles": "^2.4.1",
@@ -68,6 +72,7 @@
6872
"eslint": "^9.38.0",
6973
"get-port": "^7.1.0",
7074
"prettier": "^3.6.2",
75+
"react": "^19.2.0",
7176
"release-it": "^19.0.5",
7277
"supertest": "^7.1.4",
7378
"tsup": "^8.5.0",
@@ -81,12 +86,15 @@
8186
},
8287
"peerDependencies": {
8388
"@adonisjs/assembler": "^8.0.0-next.14",
84-
"@adonisjs/core": "^7.0.0-next.6",
89+
"@adonisjs/core": "^7.0.0-next.10",
8590
"@adonisjs/session": "^8.0.0-next.0",
8691
"@adonisjs/vite": "^5.1.0-next.0",
92+
"@inertiajs/react": "^2.2.15",
8793
"@japa/api-client": "^3.1.0",
8894
"@japa/plugin-adonisjs": "^5.0.0-next.0",
89-
"edge.js": "^6.0.0"
95+
"@tuyau/core": "^1.0.0-beta.1",
96+
"edge.js": "^6.0.0",
97+
"react": "^19.2.0"
9098
},
9199
"peerDependenciesMeta": {
92100
"@adonisjs/assembler": {
@@ -97,6 +105,15 @@
97105
},
98106
"@japa/plugin-adonisjs": {
99107
"optional": true
108+
},
109+
"react": {
110+
"optional": true
111+
},
112+
"@tuyau/core": {
113+
"optional": true
114+
},
115+
"@inertiajs/react": {
116+
"optional": true
100117
}
101118
},
102119
"keywords": [
@@ -153,7 +170,8 @@
153170
"./src/inertia_middleware.ts",
154171
"./providers/inertia_provider.ts",
155172
"./src/plugins/edge/plugin.ts",
156-
"./src/plugins/japa/api_client.ts"
173+
"./src/plugins/japa/api_client.ts",
174+
"./src/client/react/index.tsx"
157175
],
158176
"outDir": "./build",
159177
"clean": true,

src/client/react/context.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* @adonisjs/inertia
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import React from 'react'
11+
import type { Tuyau } from '@tuyau/core/client'
12+
import type { AdonisRegistry } from '@tuyau/core/types'
13+
14+
/**
15+
* React context for providing Tuyau client instance throughout the component tree
16+
*/
17+
const TuyauContext = React.createContext<Tuyau<any> | null>(null)
18+
19+
/**
20+
* Provider component that makes the Tuyau client available to child components.
21+
*
22+
* This component should wrap your entire application or the part of your
23+
* application that needs access to type-safe routing functionality.
24+
*
25+
*/
26+
export function TuyauProvider<R extends AdonisRegistry>(props: {
27+
children: React.ReactNode
28+
client: Tuyau<R>
29+
}) {
30+
return <TuyauContext.Provider value={props.client}>{props.children}</TuyauContext.Provider>
31+
}
32+
33+
/**
34+
* Hook to access the Tuyau client from any component within a TuyauProvider.
35+
*
36+
* Provides type-safe access to route generation and navigation utilities.
37+
* Must be used within a component tree wrapped by TuyauProvider.
38+
*
39+
* @returns The Tuyau client instance with full type safety
40+
* @throws Error if used outside of a TuyauProvider
41+
*/
42+
43+
export function useTuyau() {
44+
const context = React.useContext(TuyauContext)
45+
if (!context) throw new Error('You must wrap your app in a TuyauProvider')
46+
47+
return context
48+
}

src/client/react/index.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
* @adonisjs/inertia
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
export * from './context.tsx'
11+
export * from './router.ts'
12+
export * from './link.tsx'

src/client/react/link.tsx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/*
2+
* @adonisjs/inertia
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import React from 'react'
11+
import type { UserRegistry } from '@tuyau/core/types'
12+
import { Link as InertiaLink } from '@inertiajs/react'
13+
import { useTuyau } from './context.tsx'
14+
import { AreAllOptional } from '@poppinss/utils/types'
15+
16+
/**
17+
* Get parameter tuple type for a route
18+
*/
19+
type ExtractParamsTuple<Route extends keyof UserRegistry> =
20+
UserRegistry[Route]['types']['paramsTuple']
21+
22+
/**
23+
* Get parameter object type for a route
24+
*/
25+
type ExtractParamsObject<Route extends keyof UserRegistry> = UserRegistry[Route]['types']['params']
26+
27+
/**
28+
* Get params format for a route
29+
*/
30+
type RouteParamsFormats<Route extends keyof UserRegistry> =
31+
ExtractParamsObject<Route> extends Record<string, never>
32+
? never
33+
: ExtractParamsTuple<Route> | ExtractParamsObject<Route>
34+
35+
/**
36+
* Parameters required for route navigation with proper type safety.
37+
*/
38+
export type LinkParams<Route extends keyof UserRegistry> = {
39+
route: Route
40+
} & (RouteParamsFormats<Route> extends never
41+
? { params?: never }
42+
: AreAllOptional<ExtractParamsObject<Route>> extends true
43+
? { params?: RouteParamsFormats<Route> }
44+
: { params: RouteParamsFormats<Route> })
45+
46+
/**
47+
* Props for the Link component extending InertiaLink props
48+
* with route-specific type safety and parameter validation.
49+
*/
50+
type LinkProps<Route extends keyof UserRegistry> = Omit<
51+
React.ComponentPropsWithoutRef<typeof InertiaLink>,
52+
'href' | 'method'
53+
> &
54+
LinkParams<Route>
55+
56+
/**
57+
* Internal Link component implementation with forward ref support.
58+
* Resolves route parameters and generates the appropriate URL and HTTP method
59+
* for Inertia navigation.
60+
*
61+
* @param props - Link properties including route and parameters
62+
* @param ref - Forward ref for the underlying InertiaLink component
63+
*/
64+
function LinkInner<Route extends keyof UserRegistry>(
65+
props: LinkProps<Route>,
66+
ref?: React.ForwardedRef<React.ElementRef<typeof InertiaLink>>
67+
) {
68+
const { route, params, ...linkProps } = props
69+
70+
const tuyau = useTuyau()
71+
const routeInfo = tuyau.getRoute(props.route, { params })
72+
73+
return (
74+
<InertiaLink
75+
{...linkProps}
76+
href={routeInfo.url}
77+
method={routeInfo.methods[0].toLowerCase() as any}
78+
ref={ref}
79+
/>
80+
)
81+
}
82+
83+
/**
84+
* Type-safe Link component for Inertia.js navigation.
85+
*
86+
* Provides compile-time route validation and automatic parameter type checking
87+
* based on your application's route definitions. Automatically resolves the
88+
* correct URL and HTTP method for each route.
89+
*
90+
* @example
91+
* ```tsx
92+
* // Link to a route without parameters
93+
* <Link route="home">Home</Link>
94+
*
95+
* // Link to a route with required parameters
96+
* <Link route="user.show" params={{ id: 1 }}>
97+
* View User
98+
* </Link>
99+
* ```
100+
*/
101+
102+
export const Link: <Route extends keyof UserRegistry>(
103+
props: LinkProps<Route> & {
104+
ref?: React.Ref<React.ElementRef<typeof InertiaLink>>
105+
}
106+
) => ReturnType<typeof LinkInner> = React.forwardRef(LinkInner as any) as any

src/client/react/router.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* @adonisjs/inertia
3+
*
4+
* (c) AdonisJS
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import type { UserRegistry } from '@tuyau/core/types'
11+
import { router as InertiaRouter } from '@inertiajs/react'
12+
13+
import { useTuyau } from './context.tsx'
14+
import type { LinkParams } from './link.tsx'
15+
16+
/**
17+
* Custom hook providing type-safe navigation utilities for Inertia.js.
18+
*
19+
* Returns an enhanced router object with type-safe navigation methods
20+
* that automatically resolve route URLs and HTTP methods based on
21+
* your application's route definitions.
22+
*
23+
* @returns Router object with type-safe navigation methods
24+
*/
25+
export function useRouter() {
26+
const tuyau = useTuyau()
27+
28+
return {
29+
/**
30+
* Navigate to a route with type-safe parameters and options.
31+
*
32+
* Automatically resolves the route URL and HTTP method based on the
33+
* route definition, then performs the navigation using Inertia's router.
34+
*
35+
* @param props - Route navigation parameters including route name and params
36+
* @param options - Optional Inertia visit options for controlling navigation behavior
37+
*
38+
* @example
39+
* ```tsx
40+
* // Navigate to a simple route
41+
* router.visit({ route: 'dashboard' })
42+
*
43+
* // Navigate with parameters
44+
* router.visit({ route: 'user.edit', params: { id: userId } })
45+
* ```
46+
*/
47+
visit: <Route extends keyof UserRegistry>(
48+
props: LinkParams<Route>,
49+
options?: Parameters<typeof InertiaRouter.visit>[1]
50+
) => {
51+
const routeInfo = tuyau.getRoute(props.route, { params: props.params })
52+
const url = routeInfo.url
53+
54+
return InertiaRouter.visit(url, {
55+
...options,
56+
method: routeInfo.methods[0].toLowerCase() as any,
57+
})
58+
},
59+
}
60+
}

0 commit comments

Comments
 (0)