Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/query-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"@tuyau/core": "^1.0.0-beta.9"
},
"devDependencies": {
"@tanstack/query-core": "^5.90.20",
"@tanstack/query-core": "^5.96.2",
"@tuyau/core": "workspace:*"
},
"tsup": {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"@adonisjs/core": "^7.0.1",
"@faker-js/faker": "^10.3.0",
"@happy-dom/global-registrator": "^20.8.4",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-query": "^5.96.2",
"@testing-library/react": "^16.3.2",
"@tuyau/core": "workspace:*",
"@types/react": "^19.2.14",
Expand Down
23 changes: 23 additions & 0 deletions packages/svelte-query/bin/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { assert } from '@japa/assert'
import { snapshot } from '@japa/snapshot'
import { fileSystem } from '@japa/file-system'
import { expectTypeOf } from '@japa/expect-type'
import { processCLIArgs, configure, run } from '@japa/runner'

import { HappyDom } from '../tests/helpers/happy_dom_env.ts'

processCLIArgs(process.argv.slice(2))
configure({
files: ['tests/**/*.spec.ts'],
plugins: [assert(), expectTypeOf(), fileSystem({ autoClean: true }), snapshot()],
setup: [
() => {
HappyDom.init()
return () => {
HappyDom.destroy()
}
},
],
})

run()
79 changes: 79 additions & 0 deletions packages/svelte-query/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"name": "@tuyau/svelte-query",
"type": "module",
"version": "1.1.0",
"description": "Svelte Tanstack Query integration for Tuyau",
"author": "Julien Ripouteau <julien@ripouteau.com>",
"license": "MIT",
"homepage": "https://github.com/Julien-R44/tuyau#readme",
"repository": {
"type": "git",
"url": "git+https://github.com/Julien-R44/tuyau.git",
"directory": "packages/svelte-query"
},
"bugs": {
"url": "https://github.com/Julien-R44/tuyau/issues"
},
"keywords": [
"adonisjs",
"typescript",
"typesafe",
"svelte-query",
"tanstack-query",
"tuyau",
"svelte"
],
"exports": {
".": "./build/index.js"
},
"main": "build/index.js",
"files": [
"build"
],
"engines": {
"node": ">=24.0.0"
},
"scripts": {
"lint": "oxlint .",
"typecheck": "tsc --noEmit",
"build": "tsup-node",
"test": "pnpm quick:test && pnpm test:integration",
"quick:test": "node --import=@poppinss/ts-exec --enable-source-maps bin/test.ts --force-exit",
"test:integration": "vitest run",
"checks": "pnpm lint && pnpm typecheck"
},
"dependencies": {
"@tuyau/query-core": "workspace:*"
},
"peerDependencies": {
"@tanstack/svelte-query": "^6.1.0",
"@tuyau/core": "^1.0.0-beta.9",
"svelte": "^5.0.0"
},
"devDependencies": {
"@adonisjs/core": "^7.0.1",
"@faker-js/faker": "^10.3.0",
"@happy-dom/global-registrator": "^20.8.4",
"@tanstack/query-core": "^5.96.2",
"@tanstack/svelte-query": "^6.1.13",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tuyau/core": "workspace:*",
"nock": "^14.0.11",
"svelte": "^5.25.0",
"vitest": "^4.1.0"
},
"tsup": {
"entry": [
"./src/index.ts"
],
"outDir": "./build",
"clean": true,
"format": "esm",
"dts": true,
"target": "esnext"
},
"publishConfig": {
"access": "public",
"tag": "latest"
}
}
88 changes: 88 additions & 0 deletions packages/svelte-query/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { setContext, getContext } from 'svelte'
import type { Tuyau } from '@tuyau/core/client'
import type { AdonisEndpoint, InferRoutes, TuyauRegistry } from '@tuyau/core/types'

import { createTuyauSvelteQueryClient } from './main.ts'
import type { TuyauSvelteQuery } from './types/common.ts'

const TUYAU_KEY = Symbol('tuyau')
const TUYAU_CLIENT_KEY = Symbol('tuyau-client')

/**
* Options for setting up Tuyau context in a Svelte component tree
*/
export interface SetTuyauContextOptions<
TRegistry extends TuyauRegistry,
TRoutes extends Record<string, AdonisEndpoint> = InferRoutes<TRegistry>,
> {
client: Tuyau<TRegistry, TRoutes>
}

/**
* Sets up the Tuyau context in a Svelte component tree.
* Must be called during component initialization (in `<script>` of a parent component).
*
* @example
* ```svelte
* <script>
* import { setTuyauContext } from '@tuyau/svelte-query'
* import { client } from './tuyau'
*
* setTuyauContext({ client })
* </script>
* ```
*/
export function setTuyauContext<
TRegistry extends TuyauRegistry,
TRoutes extends Record<string, AdonisEndpoint> = InferRoutes<TRegistry>,
>(options: SetTuyauContextOptions<TRegistry, TRoutes>) {
const queryClient = createTuyauSvelteQueryClient({ client: options.client })
setContext(TUYAU_KEY, queryClient)
setContext(TUYAU_CLIENT_KEY, options.client)
return queryClient
}

/**
* Retrieves the Tuyau Svelte Query client from context.
* Must be called during component initialization.
*
* @example
* ```svelte
* <script>
* import { useTuyau } from '@tuyau/svelte-query'
* import { createQuery } from '@tanstack/svelte-query'
*
* const tuyau = useTuyau()
* const query = createQuery(() => tuyau.users.index.queryOptions())
* </script>
* ```
*/
export function useTuyau<
TRegistry extends TuyauRegistry,
TRoutes extends Record<string, AdonisEndpoint> = InferRoutes<TRegistry>,
>(): TuyauSvelteQuery<TRoutes> {
const ctx = getContext<TuyauSvelteQuery<TRoutes> | undefined>(TUYAU_KEY)
if (!ctx) {
throw new Error(
'useTuyau() must be called during component initialization after setTuyauContext()',
)
}
return ctx
}

/**
* Retrieves the raw Tuyau client from context.
* Must be called during component initialization.
*/
export function useTuyauClient<
TRegistry extends TuyauRegistry,
TRoutes extends Record<string, AdonisEndpoint> = InferRoutes<TRegistry>,
>(): Tuyau<TRegistry, TRoutes> {
const client = getContext<Tuyau<TRegistry, TRoutes> | undefined>(TUYAU_CLIENT_KEY)
if (!client) {
throw new Error(
'useTuyauClient() must be called during component initialization after setTuyauContext()',
)
}
return client
}
6 changes: 6 additions & 0 deletions packages/svelte-query/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export * from './types/common.ts'
export * from './main.ts'
export * from './mutation.ts'
export * from './query.ts'
export * from './infinite_query.ts'
export * from './context.ts'
47 changes: 47 additions & 0 deletions packages/svelte-query/src/infinite_query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Tuyau } from '@tuyau/core/client'
import { createInfiniteQueryFn } from '@tuyau/query-core'
import type { TuyauQueryKey, TuyauRequestOptions } from '@tuyau/query-core'
import type { SkipToken } from '@tanstack/query-core'
import type { RawRequestArgs } from '@tuyau/core/types'
import type { CreateInfiniteQueryOptions } from '@tanstack/svelte-query'

import type { AnyTuyauInfiniteQueryOptionsIn } from './types/common.ts'

/**
* Internal options for building an infinite query options object
*/
interface TuyauInfiniteQueryOptionsOptions {
request: RawRequestArgs<any> | SkipToken
opts?: AnyTuyauInfiniteQueryOptionsIn<any, any, any>
queryKey: TuyauQueryKey
routeName: string
client: Tuyau<any>
globalOptions?: TuyauRequestOptions
}

/**
* Builds a TanStack Svelte Query `infiniteQueryOptions` object from Tuyau route information.
* Delegates queryFn creation to the shared `createInfiniteQueryFn` from `@tuyau/query-core`.
*
* @tanstack/svelte-query's barrel export re-exports .svelte component files
* (HydrationBoundary, QueryClientProvider) which Node.js cannot load without
* a Svelte compiler. Since `infiniteQueryOptions` is a pure identity function
* (it just returns its input for type inference), we construct the result directly
*/
export function tuyauInfiniteQueryOptions(
options: TuyauInfiniteQueryOptionsOptions,
): CreateInfiniteQueryOptions & { queryKey: TuyauQueryKey } {
const { request, routeName, opts, queryKey, client, globalOptions } = options

const queryFn = createInfiniteQueryFn({ request, routeName, opts, client, globalOptions })

return {
...opts,
queryKey,
queryFn,

initialPageParam: opts?.initialPageParam ?? 1,
getNextPageParam: opts?.getNextPageParam ?? (() => null),
getPreviousPageParam: opts?.getPreviousPageParam,
} as any
}
96 changes: 96 additions & 0 deletions packages/svelte-query/src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Tuyau } from '@tuyau/core/client'
import { buildKey, segmentsToRouteName, getMutationKeyInternal } from '@tuyau/query-core'
import type { QueryFilters } from '@tanstack/query-core'
import type {
AdonisEndpoint,
InferRoutes,
InferTree,
RawRequestArgs,
TuyauRegistry,
} from '@tuyau/core/types'
import type { TuyauQueryKey, TuyauRequestOptions } from '@tuyau/query-core'

import { tuyauQueryOptions } from './query.ts'
import { tuyauInfiniteQueryOptions } from './infinite_query.ts'
import { tuyauMutationOptions } from './mutation.ts'
import type { TransformToSvelteQuery } from './types/common.ts'

/**
* Creates a type-safe TanStack Svelte Query client from a Tuyau client instance.
* Returns a Proxy-based object that mirrors the API route tree and exposes
* `queryOptions`, `mutationOptions`, `infiniteQueryOptions`, and key/filter
* helpers on each endpoint node.
*
* GET/HEAD endpoints get query and infinite query methods.
* Other methods (POST, PUT, etc.) get mutation methods.
* All nodes get `pathKey` and `pathFilter` for cache operations
*/
export function createTuyauSvelteQueryClient<
Reg extends TuyauRegistry,
Tree = InferTree<Reg>,
Routes extends Record<string, AdonisEndpoint> = InferRoutes<Reg>,
>(options: {
client: Tuyau<Reg, Routes>
globalOptions?: TuyauRequestOptions
}): TransformToSvelteQuery<Tree> {
const { client, globalOptions } = options

function makeSvelteQueryNamed(segments: string[]): any {
const routeName = segmentsToRouteName(segments)
const decoratedEndpoint = {
queryOptions: (request: RawRequestArgs<any>, opts?: any) => {
return tuyauQueryOptions({
opts,
client,
request,
routeName,
globalOptions,
queryKey: buildKey({ segments, request, type: 'query' }),
})
},

queryKey: (request: RawRequestArgs<any>) => buildKey({ segments, request, type: 'query' }),
queryFilter: (request?: RawRequestArgs<any>, filters?: QueryFilters<TuyauQueryKey>) => ({
queryKey: buildKey({ segments, request, type: 'query' }),
...filters,
}),

infiniteQueryOptions: (request: RawRequestArgs<any>, opts?: any) => {
return tuyauInfiniteQueryOptions({
opts,
client,
request,
routeName,
globalOptions,
queryKey: buildKey({ segments, request, type: 'infinite' }),
})
},

infiniteQueryKey: (request: RawRequestArgs<any>) =>
buildKey({ segments, request, type: 'infinite' }),
infiniteQueryFilter: (request?: RawRequestArgs<any>, filters?: QueryFilters<any>) => ({
queryKey: buildKey({ segments, request, type: 'infinite' }),
...filters,
}),

mutationOptions: (opts?: any) => tuyauMutationOptions({ opts, client, routeName }),
mutationKey: () => getMutationKeyInternal({ segments }),

pathKey: () => buildKey({ segments, type: 'any' }),
pathFilter: (filters?: QueryFilters<TuyauQueryKey>) => ({
queryKey: buildKey({ segments, type: 'any' }),
...filters,
}),
}

return new Proxy(decoratedEndpoint, {
get: (target, prop) => {
if (typeof prop === 'symbol') return undefined
if (prop in target) return target[prop as keyof typeof target]
return makeSvelteQueryNamed([...segments, String(prop)])
},
})
}

return makeSvelteQueryNamed([]) as TransformToSvelteQuery<Tree>
}
Loading