feat: add custom Nuxt client

This commit is contained in:
wobsoriano
2022-12-18 15:35:56 -08:00
parent d8d4c92ae8
commit ee85f3ccd1
9 changed files with 901 additions and 124 deletions

1
client.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
export * from './dist/client/index'

View File

@@ -9,13 +9,19 @@
".": { ".": {
"require": "./dist/index.cjs", "require": "./dist/index.cjs",
"import": "./dist/index.mjs" "import": "./dist/index.mjs"
},
"./client": {
"types": "./dist/client/index.d.ts",
"require": "./dist/client/index.cjs",
"import": "./dist/client/index.mjs"
} }
}, },
"main": "./dist/index.mjs", "main": "./dist/index.mjs",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"files": [ "files": [
"dist" "dist",
"client.d.ts"
], ],
"scripts": { "scripts": {
"dev": "concurrently \"pnpm build -- --watch\" \"pnpm --filter playground dev\"", "dev": "concurrently \"pnpm build -- --watch\" \"pnpm --filter playground dev\"",
@@ -38,8 +44,8 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/eslint-config-typescript": "^11.0.0", "@nuxtjs/eslint-config-typescript": "^11.0.0",
"@trpc/client": "^10.1.0", "@trpc/client": "^10.5.0",
"@trpc/server": "^10.1.0", "@trpc/server": "^10.5.0",
"bumpp": "^8.2.1", "bumpp": "^8.2.1",
"concurrently": "^7.5.0", "concurrently": "^7.5.0",
"eslint": "^8.25.0", "eslint": "^8.25.0",

View File

@@ -9,8 +9,8 @@
"postinstall": "nuxt prepare" "postinstall": "nuxt prepare"
}, },
"dependencies": { "dependencies": {
"@trpc/client": "^10.1.0", "@trpc/client": "^10.5.0",
"@trpc/server": "^10.1.0", "@trpc/server": "^10.5.0",
"superjson": "^1.11.0", "superjson": "^1.11.0",
"trpc-nuxt": "workspace:*", "trpc-nuxt": "workspace:*",
"zod": "^3.19.1" "zod": "^3.19.1"

View File

@@ -1,15 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { TRPCClientError } from '@trpc/client';
import type { inferRouterOutputs } from '@trpc/server';
import type { AppRouter } from '~~/server/trpc/routers';
const { $client } = useNuxtApp() const { $client } = useNuxtApp()
const { mutate } = $client.todo.addTodo.useMutation()
const addTodo = async () => { const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7) const title = Math.random().toString(36).slice(2, 7)
try { try {
const x = await $client.todo.addTodo.mutate({ const x = await mutate({
id: Date.now(), id: Date.now(),
userId: 69, userId: 69,
title, title,
@@ -21,10 +18,7 @@ const addTodo = async () => {
} }
} }
type RouterOutput = inferRouterOutputs<AppRouter>; const { data: todos, pending, error, refresh } = await $client.todo.getTodos.useQuery()
type ErrorOutput = TRPCClientError<AppRouter>
const { data: todos, pending, error, refresh } = await useAsyncData<RouterOutput['todo']['getTodos'], ErrorOutput>(() => $client.todo.getTodos.query())
</script> </script>
<template> <template>

View File

@@ -1,23 +1,62 @@
import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client' import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'
import superjson from 'superjson' import superjson from 'superjson'
import { FetchError } from 'ofetch'
import { createTRPCNuxtClient } from 'trpc-nuxt/client'
import type { AppRouter } from '~~/server/trpc/routers' import type { AppRouter } from '~~/server/trpc/routers'
export default defineNuxtPlugin(() => { export default defineNuxtPlugin(() => {
const headers = useRequestHeaders() const headers = useRequestHeaders()
const client = createTRPCProxyClient<AppRouter>({ // const client = createTRPCProxyClient<AppRouter>({
// transformer: superjson,
// links: [
// // adds pretty logs to your console in development and logs errors in production
// loggerLink({
// enabled: opts =>
// process.env.NODE_ENV === 'development' ||
// (opts.direction === 'down' && opts.result instanceof Error)
// }),
// httpBatchLink({
// url: '/api/trpc',
// headers () {
// return headers
// },
// fetch: (input, options) =>
// globalThis.$fetch.raw(input.toString(), options)
// .catch((e) => {
// if (e instanceof FetchError && e.response) { return e.response }
// throw e
// })
// .then(response => ({
// ...response,
// json: () => Promise.resolve(response._data)
// }))
// })
// ]
// })
const client = createTRPCNuxtClient<AppRouter>({
transformer: superjson, transformer: superjson,
links: [ links: [
// adds pretty logs to your console in development and logs errors in production // adds pretty logs to your console in development and logs errors in production
loggerLink({ // loggerLink({
enabled: opts => // enabled: opts =>
process.env.NODE_ENV === 'development' || // process.env.NODE_ENV === 'development' ||
(opts.direction === 'down' && opts.result instanceof Error) // (opts.direction === 'down' && opts.result instanceof Error)
}), // }),
httpBatchLink({ httpBatchLink({
url: 'http://localhost:3000/api/trpc', url: '/api/trpc',
headers () { headers () {
return headers return headers
} },
fetch: (input, options) =>
$fetch.raw(input.toString(), options)
.catch((e) => {
if (e instanceof FetchError && e.response) { return e.response }
throw e
})
.then(response => ({
...response,
json: () => Promise.resolve(response._data)
}))
}) })
] ]
}) })

754
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

89
src/client/index.ts Normal file
View File

@@ -0,0 +1,89 @@
import { type CreateTRPCClientOptions, type inferRouterProxyClient, createTRPCProxyClient, httpBatchLink as _httpBatchLink } from '@trpc/client'
import { type AnyRouter } from '@trpc/server'
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import { hash } from 'ohash'
import { nanoid } from 'nanoid'
import { type DecoratedProcedureRecord } from './types'
// @ts-expect-error: Nuxt auto-imports
import { getCurrentInstance, onScopeDispose, useAsyncData, useRequestHeaders, ref, unref } from '#imports'
/**
* Calculates the key used for `useAsyncData` call
*/
export function getQueryKey (
path: string,
input: unknown
): string {
return input === undefined ? path : `${path}-${hash(input || '')}`
}
export function createNuxtProxyDecoration<TRouter extends AnyRouter> (name: string, client: inferRouterProxyClient<TRouter>) {
return createRecursiveProxy((opts) => {
const args = opts.args
const pathCopy = [name, ...opts.path]
// The last arg is for instance `.useMutation` or `.useQuery()`
const lastArg = pathCopy.pop()!
// The `path` ends up being something like `post.byId`
const path = pathCopy.join('.')
const [input, otherOptions] = args
const { trpc, ...asyncDataOptions } = otherOptions || {} as any
let controller: AbortController
if (trpc?.abortOnUnmount) {
if (getCurrentInstance()) {
onScopeDispose(() => {
controller?.abort?.()
})
}
controller = typeof AbortController !== 'undefined' ? new AbortController() : {} as AbortController
}
if (lastArg === 'useQuery') {
const queryKey = getQueryKey(path, input)
return useAsyncData(queryKey, () => (client as any)[path].query(input, {
signal: controller?.signal,
...trpc
}), asyncDataOptions)
}
if (lastArg === 'useMutation') {
const reactiveInput = ref(null)
const { refresh, ...result } = useAsyncData(nanoid(), () => (client as any)[path].mutate(reactiveInput.value, {
signal: controller?.signal,
...trpc
}), {
...asyncDataOptions,
immediate: false
})
async function mutate (_input: any) {
reactiveInput.value = _input
await refresh()
return unref(result.data)
}
return {
mutate,
...result
}
}
return (client as any)[path][lastArg](input)
})
}
export function createTRPCNuxtClient<TRouter extends AnyRouter> (opts: CreateTRPCClientOptions<TRouter>) {
const client = createTRPCProxyClient<TRouter>(opts)
const decoratedClient = createFlatProxy((key) => {
return createNuxtProxyDecoration(key, client)
}) as DecoratedProcedureRecord<TRouter['_def']['record'], TRouter>
return decoratedClient
}

94
src/client/types.ts Normal file
View File

@@ -0,0 +1,94 @@
import type { TRPCClientErrorLike, TRPCRequestOptions as _TRPCRequestOptions } from '@trpc/client'
import { type TRPCSubscriptionObserver } from '@trpc/client/dist/internals/TRPCClient'
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
AnyRouter,
ProcedureRouterRecord,
inferProcedureInput,
inferProcedureOutput,
ProcedureArgs,
AnySubscriptionProcedure
} from '@trpc/server'
import { type inferObservableValue, type Unsubscribable } from '@trpc/server/observable'
import { inferTransformedProcedureOutput } from '@trpc/server/shared'
// import { inferTransformedProcedureOutput } from '@trpc/server/shared'
import type {
AsyncData,
AsyncDataOptions,
KeyOfRes,
PickFrom,
_Transform
} from 'nuxt/dist/app/composables/asyncData'
// Modified @trpc/client and @trpc/react-query types
// https://github.com/trpc/trpc/blob/next/packages/client/src/createTRPCClientProxy.ts
// https://github.com/trpc/trpc/blob/next/packages/react-query/src/createTRPCReact.tsx
interface TRPCRequestOptions extends _TRPCRequestOptions {
abortOnUnmount?: boolean
}
type Resolver<TProcedure extends AnyProcedure> = (
...args: ProcedureArgs<TProcedure['_def']>
) => Promise<inferTransformedProcedureOutput<TProcedure>>;
type SubscriptionResolver<
TProcedure extends AnyProcedure,
TRouter extends AnyRouter,
> = (
...args: [
input: ProcedureArgs<TProcedure['_def']>[0],
opts: ProcedureArgs<TProcedure['_def']>[1] &
Partial<
TRPCSubscriptionObserver<
inferObservableValue<inferProcedureOutput<TProcedure>>,
TRPCClientErrorLike<TRouter>
>
>,
]
) => Unsubscribable
type DecorateProcedure<
TProcedure extends AnyProcedure,
TRouter extends AnyRouter,
> = TProcedure extends AnyQueryProcedure
? {
useQuery: <
TData = inferTransformedProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc?: TRPCRequestOptions },
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>,
query: Resolver<TProcedure>
} : TProcedure extends AnyMutationProcedure ? {
useMutation: <
TData = inferTransformedProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
opts?: AsyncDataOptions<TData, Transform, PickKeys> & { trpc?: TRPCRequestOptions },
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>> & {
mutate: (input: inferProcedureInput<TProcedure>) => Promise<PickFrom<ReturnType<Transform>, PickKeys>>
},
mutate: Resolver<TProcedure>
} : TProcedure extends AnySubscriptionProcedure ? {
subscribe: SubscriptionResolver<TProcedure, TRouter>
} : never
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TRouter extends AnyRouter,
> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<TProcedures[TKey]['_def']['record'], TRouter>
: TProcedures[TKey] extends AnyProcedure
? DecorateProcedure<TProcedures[TKey], TRouter>
: never;
}

View File

@@ -1,11 +1,11 @@
import { defineConfig } from 'tsup' import { defineConfig } from 'tsup'
export default defineConfig({ export default defineConfig({
entry: ['src/index.ts'], entry: ['src/index.ts', 'src/client/index.ts'],
format: ['cjs', 'esm'], format: ['cjs', 'esm'],
splitting: false, splitting: false,
clean: true, clean: true,
external: ['#app', '#imports'], external: ['#app', '#imports', /@trpc\/client/, /@trpc\/server/],
dts: true, dts: true,
outExtension ({ format }) { outExtension ({ format }) {
return { return {