Compare commits

..

13 Commits

Author SHA1 Message Date
Robert Soriano
88c77f6e8f release v0.1.2 2022-05-20 00:50:03 -07:00
Robert Soriano
eafc476544 fix types 2022-05-20 00:49:56 -07:00
Robert Soriano
dc86c0252e fix: build error 2022-05-20 00:47:21 -07:00
Robert Soriano
a3dadb9e50 fix demo gif 2022-05-19 14:11:22 -07:00
Robert Soriano
5331535237 update demo gif 2022-05-19 14:07:48 -07:00
Robert Soriano
ae0abd2b45 add demo gif 2022-05-19 14:05:44 -07:00
Robert Soriano
273215c9e1 update readme 2022-05-19 10:56:54 -07:00
Robert Soriano
812ceda4a0 update merging routers example 2022-05-19 10:54:08 -07:00
Robert Soriano
c48556e24e add merging routers recipe 2022-05-19 10:50:50 -07:00
Robert Soriano
f3e0165dd8 update dev playground 2022-05-19 10:26:13 -07:00
Robert Soriano
87ed453425 update playground 2022-05-19 10:05:17 -07:00
Robert Soriano
d890633cb1 remove test line 2022-05-19 09:37:37 -07:00
Robert Soriano
a48ee2551e update local pnpm to 7.1.1 2022-05-19 09:26:22 -07:00
12 changed files with 213 additions and 140 deletions

View File

@@ -4,6 +4,17 @@
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
<p align="center">
<figure>
<img src="https://i.imgur.com/AjmNUxj.gif" alt="Demo" />
<figcaption>
<p align="center">
The client above is <strong>not</strong> importing any code from the server, only its type declarations.
</p>
</figcaption>
</figure>
</p>
## Install
```bash
@@ -63,11 +74,9 @@ const client = useClient() // auto-imported
const users = await client.query('getUsers')
const addUser = async () => {
const newUser = await client.mutation('createUser', {
name: 'wagmi'
})
}
const newUser = await client.mutation('createUser', {
name: 'wagmi'
})
```
## useAsyncQuery
@@ -86,7 +95,7 @@ const {
refresh
} = await useAsyncQuery(['getUser', { id: 69 }], {
// pass useAsyncData options here
server: true
lazy: false
})
```
@@ -131,6 +140,7 @@ export const onError = (payload: OnErrorPayload<typeof router>) => {
- [Validation](/recipes/validation.md)
- [Authorization](/recipes/authorization.md)
- [Merging Routers](/recipes/merging-routers.md)
- [Error Handling](/recipes/error-handling.md)
- [Error Formatting](/recipes/error-formatting.md)

View File

@@ -1,8 +1,8 @@
{
"name": "trpc-nuxt",
"type": "module",
"version": "0.1.1",
"packageManager": "pnpm@7.1.0",
"version": "0.1.2",
"packageManager": "pnpm@7.1.1",
"license": "MIT",
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
@@ -25,6 +25,7 @@
"prepublishOnly": "nr build",
"build": "nuxt-module-build",
"play": "nr build && nuxi dev playground",
"build:playground": "nuxi build playground",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp --commit --push --tag && pnpm publish",
@@ -49,7 +50,7 @@
"eslint": "^8.14.0",
"nuxt": "^3.0.0-rc.3",
"ohash": "^0.1.0",
"pnpm": "^7.1.0",
"pnpm": "^7.1.1",
"superjson": "^1.9.1",
"trpc-nuxt": "workspace:*",
"zod": "^3.16.0"

View File

@@ -1,18 +1,5 @@
<script setup lang="ts">
const { data, pending, error } = await useAsyncQuery(['getUser', { username: 'jcena' }], {
lazy: false,
transform: data => data || null,
})
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error">
Error: {{ JSON.stringify(error, null, 2) }}
</div>
<div v-else>
User {{ JSON.stringify(data, null, 2) }}
<div>
<NuxtPage />
</div>
</template>

View File

@@ -1,5 +1,5 @@
import { defineNuxtConfig } from 'nuxt'
import Module from '..'
import Module from '../src/module'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
const client = useClient()
const { data: todos, pending, error, refresh } = await useAsyncQuery(['getTodos'])
const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7)
try {
const result = await client.mutation('addTodo', {
id: Date.now(),
userId: 69,
title,
completed: false,
})
console.log('Todo: ', result)
}
catch (e) {
console.log(e)
}
}
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
Error: {{ error.data.code }}
</div>
<div v-else>
<ul>
<li v-for="t in todos.slice(0, 10)" :key="t.id">
<NuxtLink :class="{ completed: t.completed }" :to="`/todo/${t.id}`">
Title: {{ t.title }}
</NuxtLink>
</li>
</ul>
<button @click="addTodo">
Add Todo
</button>
<button @click="() => refresh()">
Refresh
</button>
</div>
</template>
<style>
a {
text-decoration: none;
}
.completed {
text-decoration: line-through;
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
const route = useRoute()
const { data: todo, pending, error } = await useAsyncQuery(['getTodo', Number(route.params.id)])
</script>
<template>
<div v-if="pending">
Loading...
</div>
<div v-else-if="error?.data?.code">
{{ error.data.code }}
</div>
<div v-else>
ID: {{ todo.id }} <br>
Title: {{ todo.title }} <br>
Completed: {{ todo.completed }}
</div>
</template>

View File

@@ -1,82 +1,35 @@
// ~/server/trpc/index.ts
import { ZodError, z } from 'zod'
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import type { CompatibilityEvent } from 'h3'
import type { ResponseMetaFnPayload } from 'trpc-nuxt/api'
// import superjson from 'superjson'
import { z } from 'zod'
const fakeUsers = [
{ id: 1, username: 'jcena' },
{ id: 2, username: 'dbatista' },
{ id: 3, username: 'jbiden' },
]
const baseURL = 'https://jsonplaceholder.typicode.com'
export const router = trpc
.router<inferAsyncReturnType<typeof createContext>>()
.formatError(({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST'
&& error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
}
})
.query('getUsers', {
resolve() {
return fakeUsers
const TodoShape = z.object({
userId: z.number(),
id: z.number(),
title: z.string(),
completed: z.boolean(),
})
export type Todo = z.infer<typeof TodoShape>
export const router = trpc.router()
.query('getTodos', {
async resolve() {
return await $fetch<Todo[]>(`${baseURL}/todos`)
},
})
.query('getUser', {
// validate input with Zod
input: z.object({
username: z.string().min(5),
}),
resolve(req) {
return fakeUsers.find(i => i.username === req.input.username) ?? null
.query('getTodo', {
input: z.number(),
async resolve(req) {
return await $fetch<Todo>(`${baseURL}/todos/${req.input}`)
},
})
.mutation('createUser', {
input: z.object({ username: z.string().min(5) }),
resolve(req) {
const newUser = {
id: fakeUsers.length + 1,
username: req.input.username,
}
fakeUsers.push(newUser)
return newUser
.mutation('addTodo', {
input: TodoShape,
async resolve(req) {
return await $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
},
})
export const createContext = (event: CompatibilityEvent) => {
event.res.setHeader('x-ssr', 1)
return {}
}
export const responseMeta = (opts: ResponseMetaFnPayload<any>) => {
// const nuxtApp = useNuxtApp()
// const client = useClient()
// console.log(opts)
// if (nuxtApp.nuxtState) {
// nuxtApp.nuxtState.trpc = client.runtime.transformer.serialize({
// ctx: opts.ctx,
// errors: opts.errors,
// })
// }
// else {
// nuxtApp.nuxtState = {
// trpc: client.runtime.transformer.serialize({
// ctx: opts.ctx,
// errors: opts.errors,
// }),
// }
// }
return {}
}

2
pnpm-lock.yaml generated
View File

@@ -19,7 +19,7 @@ importers:
nuxt: ^3.0.0-rc.3
ohash: ^0.1.0
pathe: ^0.3.0
pnpm: ^7.1.0
pnpm: ^7.1.1
superjson: ^1.9.1
trpc-nuxt: workspace:*
ufo: ^0.8.4

View File

@@ -0,0 +1,46 @@
# Merging Routers
Writing all API-code in your code in the same file is not a great idea. It's easy to merge routers with other routers.
Define your routes:
```ts
// ~/server/trpc/routes/posts.ts
export const posts = trpc.router()
.query('list', {
resolve() {
// ..
return []
}
})
```
```ts
// ~/server/trpc/routes/users.ts
export const users = trpc.router()
.query('list', {
resolve() {
// ..
return []
}
})
```
```ts
// ~/server/trpc/index.ts
import { users } from './routes/users'
import { posts } from './routes/posts'
export const router = trpc.router()
.merge('user.', users) // prefix user procedures with "user."
.merge('post.', posts) // prefix post procedures with "post."
```
and use it like this:
```html
<script setup lang="ts">
const { data: users } = await useAsyncQuery(['user.list'])
const { data: posts } = await useAsyncQuery(['post.list'])
</script>
```

View File

@@ -1,9 +1,8 @@
import { fileURLToPath } from 'url'
import { dirname, join } from 'pathe'
import { join } from 'pathe'
import { defu } from 'defu'
import { addServerHandler, defineNuxtModule } from '@nuxt/kit'
import fs from 'fs-extra'
import { addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
@@ -21,10 +20,10 @@ export default defineNuxtModule<ModuleOptions>({
},
async setup(options, nuxt) {
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
nuxt.options.build.transpile.push(runtimeDir)
nuxt.options.build.transpile.push(runtimeDir, '#build/trpc-client', '#build/trpc-handler')
const clientPath = join(nuxt.options.buildDir, 'trpc-client.ts')
const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts')
nuxt.options.build.transpile.push(handlerPath)
// Final resolved configuration
const finalConfig = nuxt.options.runtimeConfig.public.trpc = defu(nuxt.options.runtimeConfig.public.trpc, {
@@ -32,43 +31,53 @@ export default defineNuxtModule<ModuleOptions>({
trpcURL: options.trpcURL,
})
nuxt.hook('autoImports:extend', (imports) => {
imports.push(
{ name: 'useClient', from: '#build/trpc-client' },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
)
})
addServerHandler({
route: `${finalConfig.trpcURL}/*`,
handler: handlerPath,
})
nuxt.hook('autoImports:extend', (imports) => {
imports.push(
{ name: 'useClient', from: clientPath },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
)
addTemplate({
filename: 'trpc-client.ts',
write: true,
getContents() {
return `
import * as trpc from '@trpc/client'
import type { router } from '~/server/trpc'
const client = trpc.createTRPCClient<typeof router>({
url: '${finalConfig.baseURL}${finalConfig.trpcURL}',
})
export const useClient = () => client
`
},
})
await fs.ensureDir(dirname(clientPath))
await fs.writeFile(clientPath, `
import * as trpc from '@trpc/client'
import type { router } from '~/server/trpc'
const client = trpc.createTRPCClient<typeof router>({
url: '${finalConfig.baseURL}${finalConfig.trpcURL}',
})
addTemplate({
filename: 'trpc-handler.ts',
write: true,
getContents() {
return `
import { createTRPCHandler } from 'trpc-nuxt/api'
import { useRuntimeConfig } from '#imports'
import * as functions from '~/server/trpc'
export const useClient = () => client
`)
await fs.writeFile(handlerPath, `
import { createTRPCHandler } from 'trpc-nuxt/api'
import { useRuntimeConfig } from '#imports'
import * as functions from '~/server/trpc'
const { trpc: { trpcURL } } = useRuntimeConfig().public
export default createTRPCHandler({
...functions,
trpcURL
})
`)
const { trpc: { trpcURL } } = useRuntimeConfig().public
export default createTRPCHandler({
...functions,
trpcURL
})
`
},
})
},
})

View File

@@ -58,8 +58,6 @@ export function createTRPCHandler<Router extends AnyRouter>({
const $url = createURL(req.url)
event.context.hello = 'world'
const httpResponse = await resolveHTTPResponse({
router,
req: {

View File

@@ -8,11 +8,8 @@ import type {
import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server'
import type { TRPCClientErrorLike } from '@trpc/client'
import { objectHash } from 'ohash'
// @ts-expect-error: Resolved by Nuxt
import { useAsyncData, useState } from '#imports'
// @ts-expect-error: Resolved by Nuxt
import { useAsyncData, useState } from '#app'
import { useClient } from '#build/trpc-client'
// @ts-expect-error: Resolved by Nuxt
import type { router } from '~/server/trpc'
type AppRouter = typeof router
@@ -49,7 +46,6 @@ export async function useAsyncQuery<
options,
)
// @ts-expect-error: Resolved by Nuxt
if (process.server && error.value && !serverError.value)
serverError.value = error.value as any