Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @tuyau/core

## 1.3.0

### Minor Changes

- 91fe0f5: Export registry-local `Route` and `Path` type helpers from each generated registry. Add `typeAugmentation: false` to disable the global `UserRegistry` augmentation for secondary registries. The default remains `true` for compatibility with existing core helpers and AdonisJS Inertia.

Support multiple `generateRegistry` hooks with independent route filters, and make global and sticky regex filters repeatable across scanning and output generation.

## 1.2.3

### Patch Changes
Expand Down
54 changes: 52 additions & 2 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,59 @@

Core package for Tuyau. Includes the backend integration and the E2E typesafe client to consume your AdonisJS APIs.

## Documentation
## Registry types

Available soon.
Each generated registry exports its own `Route` and `Path` type helpers. Their names and paths are limited to that registry.

```ts
import type { Route, Path } from '~generated/registry'

type User = Route.Response<'users.show'>
type CreateUser = Route.Body<'users.store'>
type UserParams = Path.Params<'GET', '/users/:id'>
```

Both helpers provide `Request`, `Response`, `Params`, `Body`, `Query`, and `Error`. `Route` accepts a route name. `Path` accepts an HTTP method and a URL pattern.

## Multiple registries

Several `generateRegistry` hooks can generate separate registries with independent route-name filters:

```ts
import { generateRegistry } from '@tuyau/core/hooks'

// Inside defineConfig in adonisrc.ts
hooks: {
init: [
generateRegistry({
output: './.adonisjs/client/public',
routes: { except: [/^admin\./] },
typeAugmentation: false,
}),
generateRegistry({
output: './.adonisjs/client/admin',
routes: { only: [/^admin\./] },
typeAugmentation: false,
}),
],
},
```

Each client receives its own registry through `createTuyau({ registry, baseUrl })`. Type imports can coexist in the same TypeScript program:

```ts
import type { Route as PublicRoute } from '~generated/public'
import type { Route as AdminRoute } from '~generated/admin'

type User = PublicRoute.Response<'users.show'>
type Dashboard = AdminRoute.Response<'admin.dashboard'>
```

`typeAugmentation` defaults to `true` for compatibility with the global `Route` and `Path` helpers exported by `@tuyau/core/types`. At most one registry in a TypeScript program can enable it. This also applies when a `tsconfig` includes generated files without importing them.

**AdonisJS Inertia still requires the global augmentation for route typing.** Its React and Vue `Link`, `Form`, `useRouter`, and `useHttp` helpers read `UserRegistry`. Keep `typeAugmentation: true` on the registry used by Inertia and disable it on the other registries. The local helpers do not replace Inertia's global route selection.

Tuyau's React Query, Vue Query, and Svelte Query integrations infer routes from their client and do not require this augmentation.

## License

Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tuyau/core",
"type": "module",
"version": "1.2.3",
"version": "1.3.0",
"description": "E2E typesafe client for AdonisJS",
"author": "Julien Ripouteau <julien@ripouteau.com>",
"license": "MIT",
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/backend/generate_registry.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import './types.ts'

import type { AllHooks } from '@adonisjs/assembler/types'
import type { RoutesScanner } from '@adonisjs/assembler/routes_scanner'

import { RegistryGenerator } from './registry_generator.ts'
import { GenerateRegistryConfig } from './types.ts'

/**
* The scanner holds a single filter, shared by all registry generators.
*/
const generatorsByScanner = new WeakMap<RoutesScanner, Set<RegistryGenerator>>()

/**
* AdonisJS assembler hook that scans routes and generates
* the tuyau typed client registry files (runtime, schema, tree).
* Can be registered several times with different outputs and route filters.
*/
export function generateRegistry(options?: GenerateRegistryConfig): AllHooks['init'][number] {
const generator = new RegistryGenerator(options)
Expand All @@ -16,12 +23,18 @@ export function generateRegistry(options?: GenerateRegistryConfig): AllHooks['in
return {
async run(_, hooks) {
hooks.add('routesScanning', (_, routesScanner) => {
routesScanner.filter((route) => generator.filterRoute(route))
const generators = generatorsByScanner.get(routesScanner) ?? new Set()
generators.add(generator)
generatorsByScanner.set(routesScanner, generators)

routesScanner.filter((route) => [...generators].some((each) => each.filterRoute(route)))
})

hooks.add('routesScanned', async (devServer, routesScanner) => {
const startTime = process.hrtime()
const routes = routesScanner.getScannedRoutes()
const routes = routesScanner
.getScannedRoutes()
.filter((route) => generator.filterRoute(route))

await generator.writeOutput({ outputDir, routes })

Expand Down
66 changes: 55 additions & 11 deletions packages/core/src/backend/registry_generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,17 @@ const DEFAULT_VALIDATION_ERROR_TYPE = '{ errors: SimpleError[] }'
*/
export class RegistryGenerator {
#validationErrorType: string | false
#typeAugmentation: boolean
#routesFilter?: GenerateRegistryConfig['routes']

constructor(options?: Partial<Pick<GenerateRegistryConfig, 'validationErrorType' | 'routes'>>) {
constructor(
options?: Pick<GenerateRegistryConfig, 'validationErrorType' | 'typeAugmentation' | 'routes'>,
) {
this.#validationErrorType =
options?.validationErrorType === undefined
? DEFAULT_VALIDATION_ERROR_TYPE
: options.validationErrorType
this.#typeAugmentation = options?.typeAugmentation ?? true
this.#routesFilter = options?.routes
}

Expand All @@ -31,7 +35,13 @@ export class RegistryGenerator {
pattern: string | RegExp | ((routeName: string) => boolean),
): boolean {
if (typeof pattern === 'string') return routeName.includes(pattern)
if (pattern instanceof RegExp) return pattern.test(routeName)
if (pattern instanceof RegExp) {
const lastIndex = pattern.lastIndex
pattern.lastIndex = 0
const matches = pattern.test(routeName)
pattern.lastIndex = lastIndex
return matches
}
if (typeof pattern === 'function') return pattern(routeName)

return false
Expand All @@ -41,7 +51,7 @@ export class RegistryGenerator {
* Check if a route passes the configured only/except filters.
* Returns true when no filters are set.
*/
filterRoute(route: RoutesListItem): boolean {
filterRoute(route: Pick<RoutesListItem, 'name'>): boolean {
if (!this.#routesFilter) return true

const { only, except } = this.#routesFilter
Expand Down Expand Up @@ -282,12 +292,24 @@ export class RegistryGenerator {
const registryEntries = routes
.map((route) => this.generateRuntimeRegistryEntry(route))
.join(',\n')
const moduleAugmentation = this.#typeAugmentation
? `
declare module '@tuyau/core/types' {
export interface UserRegistry {
routes: typeof routes
$tree: ApiDefinition
}
}
`
: ''

return `/* eslint-disable prettier/prettier */
import type { AdonisEndpoint } from '@tuyau/core/types'
import type { Registry } from './schema.d.ts'
import type { ApiDefinition } from './tree.d.ts'

export type { Route, Path } from './schema.d.ts'

const placeholder: any = {}

const routes = {
Expand All @@ -300,14 +322,7 @@ export const registry = {
routes,
$tree: {} as ApiDefinition,
}

declare module '@tuyau/core/types' {
export interface UserRegistry {
routes: typeof routes
$tree: ApiDefinition
}
}
`
${moduleAugmentation}`
}

/**
Expand All @@ -325,6 +340,10 @@ declare module '@tuyau/core/types' {
'ExtractQueryForGet',
'ExtractRawResponse',
'ExtractResponse',
'Method',
'PathWithRegistry',
'PatternsByMethod',
'RouteWithRegistry',
]
const vineImports = ['InferInput']
if (useDefaultValidationType) vineImports.push('SimpleError')
Expand All @@ -334,12 +353,37 @@ declare module '@tuyau/core/types' {

import type { ${coreImports.join(', ')} } from '@tuyau/core/types'
import type { ${vineImports.join(', ')} } from '@vinejs/vine/types'
import type { routes } from './index.ts'

export type ParamValue = string | number | bigint | boolean

export interface Registry {
${registryEntries}
}

export namespace Route {
export type Request<Name extends keyof typeof routes> = RouteWithRegistry.Request<typeof routes, Name>
export type Response<Name extends keyof typeof routes> = RouteWithRegistry.Response<typeof routes, Name>
export type Params<Name extends keyof typeof routes> = RouteWithRegistry.Params<typeof routes, Name>
export type Body<Name extends keyof typeof routes> = RouteWithRegistry.Body<typeof routes, Name>
export type Query<Name extends keyof typeof routes> = RouteWithRegistry.Query<typeof routes, Name>
export type Error<Name extends keyof typeof routes> = RouteWithRegistry.Error<typeof routes, Name>
}

export namespace Path {
export type Request<M extends Method, P extends PatternsByMethod<typeof routes, M>> =
PathWithRegistry.Request<typeof routes, M, P>
export type Response<M extends Method, P extends PatternsByMethod<typeof routes, M>> =
PathWithRegistry.Response<typeof routes, M, P>
export type Params<M extends Method, P extends PatternsByMethod<typeof routes, M>> =
PathWithRegistry.Params<typeof routes, M, P>
export type Body<M extends Method, P extends PatternsByMethod<typeof routes, M>> =
PathWithRegistry.Body<typeof routes, M, P>
export type Query<M extends Method, P extends PatternsByMethod<typeof routes, M>> =
PathWithRegistry.Query<typeof routes, M, P>
export type Error<M extends Method, P extends PatternsByMethod<typeof routes, M>> =
PathWithRegistry.Error<typeof routes, M, P>
}
`
}

Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/backend/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ export interface GenerateRegistryConfig {
*/
output?: string

/**
* Augment the global UserRegistry used by the core Route/Path helpers
* and AdonisJS Inertia. Disable for secondary registries in the same
* TypeScript program and import their local Route/Path helpers instead.
* @default true
*/
typeAugmentation?: boolean

/**
* Routes filtering configuration
*/
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/client/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,11 @@ export type RegValues<R> = R[StrKeys<R>]

/**
* Gets URL patterns for endpoints matching a specific HTTP method
* Defers indexing until the method is resolved so concrete registries
* can forward their method and pattern generics to PathWithRegistry.
*/
export type PatternsByMethod<
Reg extends Record<string, AdonisEndpoint>,
M extends Method,
> = EndpointsByMethod<Reg, M>['pattern']
export type PatternsByMethod<Reg extends Record<string, AdonisEndpoint>, M extends Method> =
EndpointsByMethod<Reg, M> extends infer E extends AdonisEndpoint ? E['pattern'] : never

/**
* Finds an endpoint by HTTP method and URL pattern
Expand Down Expand Up @@ -395,7 +395,7 @@ type UserEndpointByName<Name extends keyof UserAdonisRegistry> = UserAdonisRegis
type FilterByMethodPathForRegistry<
Reg extends Record<string, AdonisEndpoint>,
M extends Method,
P extends ValueOf<Reg>['pattern'] & string,
P extends string,
> = {
[K in keyof Reg]: Reg[K]['pattern'] extends P
? M extends Reg[K]['methods'][number]
Expand Down
Loading