Compare commits

..

84 Commits

Author SHA1 Message Date
Robert Soriano
5e8d04741c release v0.2.1 2022-06-13 18:51:41 -07:00
Robert Soriano
1032e65a0d refactor: remove unused import in playground 2022-06-13 18:51:35 -07:00
Robert Soriano
f261d3feeb Merge pull request #12 from cawa-93/patch-1
fix: auto-import `getQueryKey`
2022-06-13 18:49:07 -07:00
Alex Kozack
b1ca09e986 fix: auto-import getQueryKey 2022-06-13 12:06:03 +03:00
Robert Soriano
b804429fc0 update readme 2022-06-12 23:32:55 -07:00
Robert Soriano
7df64296ff update readme 2022-06-12 23:32:41 -07:00
Robert Soriano
a53d823f5e release v0.2.0 2022-06-12 23:30:07 -07:00
Robert Soriano
feef3dde6b update readme 2022-06-12 23:30:03 -07:00
Robert Soriano
82ee2ce672 refactor!: replace trpcURL option with endpoint 2022-06-12 23:29:28 -07:00
Robert Soriano
f62a13766a update readme 2022-06-12 23:19:55 -07:00
Robert Soriano
c7888e81ed refactor: use addAutoImport 2022-06-12 23:18:38 -07:00
Robert Soriano
c77eb68f5d refactor: rename endpoint option 2022-06-12 23:17:13 -07:00
Robert Soriano
333539569c refactor: lint 2022-06-12 23:16:01 -07:00
Robert Soriano
e5c40f183b Merge pull request #7 from cawa-93/feat/extract-get-query-key
feat: extract `getQueryKey` helper
2022-06-12 23:08:21 -07:00
Robert Soriano
977a9e1465 release v0.1.24 2022-06-05 17:05:34 -07:00
Robert Soriano
2620379e02 feat: allow changing of src dir 2022-06-05 17:05:13 -07:00
Robert Soriano
e4f42d5322 Merge pull request #10 from nicolesmileyface/feat/support-src-dir
use nuxt srcDir instead of rootDir
2022-06-05 17:02:41 -07:00
Cole Hollant
ad28a9124e use nuxt srcDir instead of rootDir 2022-06-05 11:55:25 -04:00
cawa-93
273bda980b feat: extract getQueryKey helper
This method will be useful to get a key that can be used to reset the internal nuxt cache
2022-05-31 15:20:21 +03:00
Robert Soriano
9c8509f79c release v0.1.23 2022-05-27 08:53:18 -07:00
Robert Soriano
2ce29137ce fix: unimported composable 2022-05-27 08:41:14 -07:00
Robert Soriano
e9c5307e23 release v0.1.22 2022-05-27 01:10:10 -07:00
Robert Soriano
bbdabf544c fix: type inference bug with vueuse 2022-05-27 01:10:06 -07:00
Robert Soriano
aed10ac5b8 release v0.1.21 2022-05-27 00:56:58 -07:00
Robert Soriano
4b2c714658 fix: remove module export 2022-05-27 00:56:53 -07:00
Robert Soriano
43e9fefdbd release v0.1.20 2022-05-26 13:48:57 -07:00
Robert Soriano
dabda23976 export module id 2022-05-26 13:48:53 -07:00
Robert Soriano
af89f32275 release v0.1.19 2022-05-26 07:27:20 -07:00
Robert Soriano
1f27b871fb fix: unref import from vue 2022-05-26 07:21:42 -07:00
Robert Soriano
281e4c05a0 release v0.1.18 2022-05-24 10:01:23 -07:00
Robert Soriano
38ac520b97 fix: server storage and incorrect imports 2022-05-24 10:01:19 -07:00
Robert Soriano
96ebff619e release v0.1.17 2022-05-24 09:55:59 -07:00
Robert Soriano
f668e4a9b4 docs: update readme 2022-05-24 09:55:35 -07:00
Robert Soriano
58b0165557 update readme 2022-05-24 09:55:05 -07:00
Robert Soriano
3b5e35ef68 feat: add composable for custom client headers 2022-05-24 09:46:15 -07:00
Robert Soriano
48152ead8d refactor: remove some comments 2022-05-24 06:43:42 -07:00
Robert Soriano
7f44e049c0 release v0.1.16 2022-05-23 12:41:55 -07:00
Robert Soriano
5a71bbf1fe refactor: use trpc url from config 2022-05-23 12:41:49 -07:00
Robert Soriano
285487e9bf release v0.1.15 2022-05-23 11:24:10 -07:00
Robert Soriano
2cfa64fcc6 fix: unresolvable runtime config 2022-05-23 11:24:07 -07:00
Robert Soriano
eea5733dcd release v0.1.14 2022-05-23 11:09:16 -07:00
Robert Soriano
71bbbf2b86 fix: trpc client composable type 2022-05-23 11:08:52 -07:00
Robert Soriano
2b57ab8791 release v0.1.13 2022-05-23 11:06:58 -07:00
Robert Soriano
f8edd769f0 fix: trpc client composable type 2022-05-23 11:06:51 -07:00
Robert Soriano
419ef34de6 release v0.1.12 2022-05-23 11:03:53 -07:00
Robert Soriano
30c76b5859 fix: client type declaration 2022-05-23 11:03:50 -07:00
Robert Soriano
2575beae5d release v0.1.11 2022-05-23 10:42:19 -07:00
Robert Soriano
b09d1af30d fix: client type declaration 2022-05-23 10:42:16 -07:00
Robert Soriano
610e441db7 release v0.1.10 2022-05-23 10:31:53 -07:00
Robert Soriano
959b370729 fix: unable to resolve nuxt instance 2022-05-23 10:31:48 -07:00
Robert Soriano
c1c4e67694 release v0.1.9 2022-05-23 10:26:02 -07:00
Robert Soriano
779221d9e6 refactor: plugin arrangement 2022-05-23 10:25:34 -07:00
Robert Soriano
986b661e99 release v0.1.8 2022-05-23 10:04:13 -07:00
Robert Soriano
77325a6699 fix: headers missing 2022-05-23 10:04:06 -07:00
Robert Soriano
6dcb4ce8a6 refactor: remove useless transpiled build 2022-05-20 14:26:45 -07:00
Robert Soriano
c95d46f43a release v0.1.7 2022-05-20 11:44:02 -07:00
Robert Soriano
2844cc0bbd refactor: remove helper types 2022-05-20 11:43:58 -07:00
Robert Soriano
aeb2e1b8e3 docs: add inference helpers recipe 2022-05-20 11:42:40 -07:00
Robert Soriano
68d9eb2461 release v0.1.6 2022-05-20 11:30:09 -07:00
Robert Soriano
109a07a42d feat: add helper types 2022-05-20 11:30:04 -07:00
Robert Soriano
7775e59b0c release v0.1.5 2022-05-20 11:26:48 -07:00
Robert Soriano
c23af214a3 remove helpers 2022-05-20 11:26:43 -07:00
Robert Soriano
7851846ad5 release v0.1.4 2022-05-20 11:22:30 -07:00
Robert Soriano
f9b0aa002e refactor: type helpers 2022-05-20 11:22:19 -07:00
Robert Soriano
eb1bd0c700 feat: add trpc type helpers 2022-05-20 11:07:12 -07:00
Robert Soriano
47ba58c0b7 release v0.1.3 2022-05-20 10:19:55 -07:00
Robert Soriano
a458ed9b3f refactor: use full path for router type 2022-05-20 10:08:53 -07:00
Robert Soriano
09d043d49b docs: add recommended ide setup 2022-05-20 10:04:48 -07:00
Robert Soriano
3e47e2a389 Update README.md 2022-05-20 09:14:40 -07:00
Robert Soriano
f8d82c4af1 remove unused module 2022-05-20 08:18:29 -07:00
Robert Soriano
e31578bf97 refactor: remove unused transpile 2022-05-20 00:58:27 -07:00
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
15 changed files with 504 additions and 173 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
@@ -18,10 +29,10 @@ export default defineNuxtConfig({
modules: ['trpc-nuxt'],
trpc: {
baseURL: 'http://localhost:3000', // defaults to http://localhost:3000
trpcURL: '/api/trpc', // defaults to /api/trpc
endpoint: '/trpc', // defaults to /trpc
},
typescript: {
strict: true // set this to true to make input/output types work
strict: true // required to make input/output types work
}
})
```
@@ -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,13 +95,28 @@ const {
refresh
} = await useAsyncQuery(['getUser', { id: 69 }], {
// pass useAsyncData options here
server: true
lazy: false
})
```
## useClientHeaders
A composable that lets you add additional properties to pass to the tRPC Client. It uses `useStorage` from [@vueuse/core](https://vueuse.org/core/usestorage).
```ts
const headers = useClientHeaders()
const { data: token } = await useAsyncQuery(['auth.login', { username, password }])
headers.value.Authorization = `Bearer ${token}`
// All client calls will now include the Authorization header.
// For SSR, please follow this link https://github.com/trpc/trpc/discussions/1686
```
## Options
trpc-nuxt accepts the following options exposed under `~/server/trpc.index.ts`:
trpc-nuxt accepts the following options exposed under `~/server/trpc/index.ts`:
```ts
import * as trpc from '@trpc/server'
@@ -131,11 +155,17 @@ 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)
- [Inference Helpers](/recipes/inference-helpers.md)
Learn more about tRPC.io [here](https://trpc.io/docs).
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## License
MIT

View File

@@ -1,8 +1,8 @@
{
"name": "trpc-nuxt",
"type": "module",
"version": "0.1.1",
"packageManager": "pnpm@7.1.0",
"version": "0.2.1",
"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",
@@ -34,8 +35,10 @@
"@nuxt/kit": "^3.0.0-rc.3",
"@trpc/client": "^9.23.3",
"@trpc/server": "^9.23.2",
"@vueuse/core": "^8.5.0",
"@vueuse/nuxt": "^8.5.0",
"dedent": "^0.7.0",
"defu": "^6.0.0",
"fs-extra": "^10.1.0",
"h3": "^0.7.8",
"pathe": "^0.3.0",
"ufo": "^0.8.4"
@@ -44,17 +47,19 @@
"@antfu/eslint-config": "^0.23.1",
"@antfu/ni": "^0.16.2",
"@nuxt/module-builder": "latest",
"@types/fs-extra": "^9.0.13",
"bumpp": "^7.1.1",
"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"
},
"eslintConfig": {
"extends": "@antfu"
"extends": "@antfu",
"rules": {
"no-console": "warn"
}
}
}

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,19 @@
<script setup lang="ts">
const counter = useCookie('counter')
counter.value = counter.value || Math.round(Math.random() * 1000)
</script>
<template>
<div>
<h1> Counter: {{ counter || '-' }}</h1>
<button @click="counter = null">
reset
</button>
<button @click="counter--">
-
</button>
<button @click="counter++">
+
</button>
</div>
</template>

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
const client = useClient()
const headers = useClientHeaders()
const { data: todos, pending, error, refresh } = await useAsyncQuery(['getTodos'])
const addHeader = () => {
// headers.value.cookie = 'counter=69'
console.log(headers.value)
}
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-if="todos">
<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>
<button @click="addHeader">
Add header
</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,53 @@
// ~/server/trpc/index.ts
import { ZodError, z } from 'zod'
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import { z } from 'zod'
import type { CompatibilityEvent } from 'h3'
import type { ResponseMetaFnPayload } from 'trpc-nuxt/api'
// import superjson from 'superjson'
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<Context>()
.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) {
console.log(req.input)
return await $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
},
})
export const createContext = (event: CompatibilityEvent) => {
event.res.setHeader('x-ssr', 1)
return {}
export async function createContext(event: CompatibilityEvent) {
// Create your context based on the request object
// Will be available as `ctx` in all your resolvers
// This is just an example of something you'd might want to do in your ctx fn
// const x = useCookies(event)
console.log(event.req.headers)
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 {}
}
type Context = inferAsyncReturnType<typeof createContext>

92
pnpm-lock.yaml generated
View File

@@ -10,16 +10,17 @@ importers:
'@nuxt/module-builder': latest
'@trpc/client': ^9.23.3
'@trpc/server': ^9.23.2
'@types/fs-extra': ^9.0.13
'@vueuse/core': ^8.5.0
'@vueuse/nuxt': ^8.5.0
bumpp: ^7.1.1
dedent: ^0.7.0
defu: ^6.0.0
eslint: ^8.14.0
fs-extra: ^10.1.0
h3: ^0.7.8
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
@@ -28,8 +29,10 @@ importers:
'@nuxt/kit': 3.0.0-rc.3
'@trpc/client': 9.23.4_@trpc+server@9.23.4
'@trpc/server': 9.23.4
'@vueuse/core': 8.5.0
'@vueuse/nuxt': 8.5.0
dedent: 0.7.0
defu: 6.0.0
fs-extra: 10.1.0
h3: 0.7.8
pathe: 0.3.0
ufo: 0.8.4
@@ -37,7 +40,6 @@ importers:
'@antfu/eslint-config': 0.23.1_eslint@8.15.0
'@antfu/ni': 0.16.2
'@nuxt/module-builder': 0.1.7
'@types/fs-extra': 9.0.13
bumpp: 7.1.1
eslint: 8.15.0
nuxt: 3.0.0-rc.3
@@ -945,12 +947,6 @@ packages:
resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==}
dev: true
/@types/fs-extra/9.0.13:
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
dependencies:
'@types/node': 17.0.34
dev: true
/@types/jsdom/16.2.14:
resolution: {integrity: sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==}
dependencies:
@@ -1274,6 +1270,22 @@ packages:
resolution: {integrity: sha512-UBc1Pg1T3yZ97vsA2ueER0F6GbJebLHYlEi4ou1H5YL4KWvMOOWwpYo9/QpWq93wxKG6Wo13IY74Hcn/f7c7Bg==}
dev: true
/@vueuse/core/8.5.0:
resolution: {integrity: sha512-VEJ6sGNsPlUp0o9BGda2YISvDZbhWJSOJu5zlp2TufRGVrLcYUKr31jyFEOj6RXzG3k/H4aCYeZyjpItfU8glw==}
peerDependencies:
'@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue:
optional: true
dependencies:
'@vueuse/metadata': 8.5.0
'@vueuse/shared': 8.5.0
vue-demi: 0.12.5
dev: false
/@vueuse/head/0.7.6_vue@3.2.33:
resolution: {integrity: sha512-cOWqCkT3WiF5oEpw+VVEWUJd9RLD5rc7DmnFp3cePsejp+t7686uKD9Z9ZU7Twb7R/BI8iexKTmXo9D/F3v6UA==}
peerDependencies:
@@ -1282,6 +1294,42 @@ packages:
vue: 3.2.33
dev: true
/@vueuse/metadata/8.5.0:
resolution: {integrity: sha512-WxsD+Cd+bn+HcjpY6Dl9FJ8ywTRTT9pTwk3bCQpzEhXVYAyNczKDSahk50fCfIJKeWHhyI4B2+/ZEOxQAkUr0g==}
dev: false
/@vueuse/nuxt/8.5.0:
resolution: {integrity: sha512-riGrDwlTQbjSDxyw46oc1catXpZwzzRrEk+PTy8NQZG6uevgJ8wNuAhPVx/5oG0jYG/t2orIFfc9xGEA6uytSg==}
dependencies:
'@nuxt/kit': 3.0.0-rc.3
'@vueuse/core': 8.5.0
'@vueuse/metadata': 8.5.0
local-pkg: 0.4.1
vue-demi: 0.12.5
transitivePeerDependencies:
- '@vue/composition-api'
- esbuild
- rollup
- supports-color
- vite
- vue
- webpack
dev: false
/@vueuse/shared/8.5.0:
resolution: {integrity: sha512-qKG+SZb44VvGD4dU5cQ63z4JE2Yk39hQUecR0a9sEdJA01cx+XrxAvFKJfPooxwoiqalAVw/ktWK6xbyc/jS3g==}
peerDependencies:
'@vue/composition-api': ^1.1.0
vue: ^2.6.0 || ^3.2.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
vue:
optional: true
dependencies:
vue-demi: 0.12.5
dev: false
/abbrev/1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: true
@@ -2031,6 +2079,10 @@ packages:
engines: {node: '>=0.10'}
dev: true
/dedent/0.7.0:
resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==}
dev: false
/deep-extend/0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@@ -3216,6 +3268,7 @@ packages:
graceful-fs: 4.2.10
jsonfile: 6.1.0
universalify: 2.0.0
dev: true
/fs-memo/1.2.0:
resolution: {integrity: sha512-YEexkCpL4j03jn5SxaMHqcO6IuWuqm8JFUYhyCep7Ao89JIYmB8xoKhK7zXXJ9cCaNXpyNH5L3QtAmoxjoHW2w==}
@@ -3747,7 +3800,7 @@ packages:
dev: true
/is-module/1.0.0:
resolution: {integrity: sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=}
resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==}
dev: true
/is-negative-zero/2.0.2:
@@ -3930,6 +3983,7 @@ packages:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
dev: true
/jsx-ast-utils/3.3.0:
resolution: {integrity: sha512-XzO9luP6L0xkxwhIJMTJQpZo/eeN60K08jHdexfD569AGxeNug6UketeHXEhROoM8aR7EcUoOQmIhcJQjcuq8Q==}
@@ -6333,6 +6387,7 @@ packages:
/universalify/2.0.0:
resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==}
engines: {node: '>= 10.0.0'}
dev: true
/unplugin/0.6.3:
resolution: {integrity: sha512-CoW88FQfCW/yabVc4bLrjikN9HC8dEvMU4O7B6K2jsYMPK0l6iAnd9dpJwqGcmXJKRCU9vwSsy653qg+RK0G6A==}
@@ -6568,6 +6623,19 @@ packages:
bundle-runner: 0.0.1
dev: true
/vue-demi/0.12.5:
resolution: {integrity: sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==}
engines: {node: '>=12'}
hasBin: true
requiresBuild: true
peerDependencies:
'@vue/composition-api': ^1.0.0-rc.1
vue: ^3.0.0-0 || ^2.6.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
dev: false
/vue-eslint-parser/8.3.0_eslint@8.15.0:
resolution: {integrity: sha512-dzHGG3+sYwSf6zFBa0Gi9ZDshD7+ad14DGOdTLjruRVgZXe2J+DcZ9iUhyR48z5g1PqRa20yt3Njna/veLJL/g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}

View File

@@ -0,0 +1,80 @@
## Inference Helpers
`@trpc/server` exports the following helper types to assist with inferring these types from the `router` exported in `~/server/trpc/index.ts`:
- `inferProcedureOutput<TProcedure>`
- `inferProcedureInput<TProcedure>`
- `inferSubscriptionOutput<TRouter, TPath>`
```ts
// ~/utils/trpc.ts
import type { router } from '~/server/trpc/index.ts'
type AppRouter = typeof router
/**
* Enum containing all api query paths
*/
export type TQuery = keyof AppRouter['_def']['queries']
/**
* Enum containing all api mutation paths
*/
export type TMutation = keyof AppRouter['_def']['mutations']
/**
* Enum containing all api subscription paths
*/
export type TSubscription = keyof AppRouter['_def']['subscriptions']
/**
* This is a helper method to infer the output of a query resolver
* @example type HelloOutput = InferQueryOutput<'hello'>
*/
export type InferQueryOutput<TRouteKey extends TQuery> = inferProcedureOutput<
AppRouter['_def']['queries'][TRouteKey]
>
/**
* This is a helper method to infer the input of a query resolver
* @example type HelloInput = InferQueryInput<'hello'>
*/
export type InferQueryInput<TRouteKey extends TQuery> = inferProcedureInput<
AppRouter['_def']['queries'][TRouteKey]
>
/**
* This is a helper method to infer the output of a mutation resolver
* @example type HelloOutput = InferMutationOutput<'hello'>
*/
export type InferMutationOutput<TRouteKey extends TMutation> =
inferProcedureOutput<AppRouter['_def']['mutations'][TRouteKey]>
/**
* This is a helper method to infer the input of a mutation resolver
* @example type HelloInput = InferMutationInput<'hello'>
*/
export type InferMutationInput<TRouteKey extends TMutation> =
inferProcedureInput<AppRouter['_def']['mutations'][TRouteKey]>
/**
* This is a helper method to infer the output of a subscription resolver
* @example type HelloOutput = InferSubscriptionOutput<'hello'>
*/
export type InferSubscriptionOutput<TRouteKey extends TSubscription> =
inferProcedureOutput<AppRouter['_def']['subscriptions'][TRouteKey]>
/**
* This is a helper method to infer the asynchronous output of a subscription resolver
* @example type HelloAsyncOutput = InferAsyncSubscriptionOutput<'hello'>
*/
export type InferAsyncSubscriptionOutput<TRouteKey extends TSubscription> =
inferSubscriptionOutput<AppRouter, TRouteKey>
/**
* This is a helper method to infer the input of a subscription resolver
* @example type HelloInput = InferSubscriptionInput<'hello'>
*/
export type InferSubscriptionInput<TRouteKey extends TSubscription> =
inferProcedureInput<AppRouter['_def']['subscriptions'][TRouteKey]>
```

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,13 +1,14 @@
import { fileURLToPath } from 'url'
import { dirname, join } from 'pathe'
import { join, resolve } from 'pathe'
import { defu } from 'defu'
// @ts-expect-error: No types
import dedent from 'dedent'
import { addServerHandler, defineNuxtModule } from '@nuxt/kit'
import fs from 'fs-extra'
import { addAutoImport, addPlugin, addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
trpcURL: string
endpoint: string
}
export default defineNuxtModule<ModuleOptions>({
@@ -17,58 +18,53 @@ export default defineNuxtModule<ModuleOptions>({
},
defaults: {
baseURL: 'http://localhost:3000',
trpcURL: '/api/trpc',
endpoint: '/trpc',
},
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-handler')
const clientPath = join(nuxt.options.buildDir, 'trpc-client.ts')
const handlerPath = join(nuxt.options.buildDir, 'trpc-handler.ts')
const trpcOptionsPath = join(nuxt.options.srcDir, 'server/trpc')
// Add vueuse
nuxt.options.modules.push('@vueuse/nuxt')
// Final resolved configuration
const finalConfig = nuxt.options.runtimeConfig.public.trpc = defu(nuxt.options.runtimeConfig.public.trpc, {
baseURL: options.baseURL,
trpcURL: options.trpcURL,
endpoint: options.endpoint,
})
addAutoImport([
{ name: 'useClient', from: join(runtimeDir, 'client') },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
{ name: 'useClientHeaders', from: join(runtimeDir, 'client') },
{ name: 'getQueryKey', from: join(runtimeDir, 'client') },
])
addServerHandler({
route: `${finalConfig.trpcURL}/*`,
route: `${finalConfig.endpoint}/*`,
handler: handlerPath,
})
nuxt.hook('autoImports:extend', (imports) => {
imports.push(
{ name: 'useClient', from: clientPath },
{ name: 'useAsyncQuery', from: join(runtimeDir, 'client') },
)
})
addPlugin(resolve(runtimeDir, 'plugin'))
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 dedent`
import { createTRPCHandler } from 'trpc-nuxt/api'
import * as functions from '${trpcOptionsPath}'
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
})
`)
export default createTRPCHandler({
...functions,
endpoint: '${finalConfig.endpoint}'
})
`
},
})
},
})

View File

@@ -42,13 +42,13 @@ export function createTRPCHandler<Router extends AnyRouter>({
createContext,
responseMeta,
onError,
trpcURL,
endpoint,
}: {
router: Router
createContext?: CreateContextFn<Router>
responseMeta?: ResponseMetaFn<Router>
onError?: OnErrorFn<Router>
trpcURL: string
endpoint: string
}) {
return defineEventHandler(async (event) => {
const {
@@ -56,19 +56,17 @@ export function createTRPCHandler<Router extends AnyRouter>({
res,
} = event
const $url = createURL(req.url)
event.context.hello = 'world'
const $url = createURL(req.url!)
const httpResponse = await resolveHTTPResponse({
router,
req: {
method: req.method,
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await useBody(event),
query: $url.searchParams,
},
path: $url.pathname.substring(trpcURL.length + 1),
path: $url.pathname.substring(endpoint.length + 1),
createContext: async () => createContext?.(event),
responseMeta,
onError: (o) => {
@@ -83,8 +81,8 @@ export function createTRPCHandler<Router extends AnyRouter>({
res.statusCode = status
Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key])
headers && Object.keys(headers).forEach((key) => {
res.setHeader(key, headers[key]!)
})
return body

View File

@@ -6,18 +6,18 @@ import type {
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server'
import type { TRPCClientErrorLike } from '@trpc/client'
import type { TRPCClient, 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 { useClient } from '#build/trpc-client'
// @ts-expect-error: Resolved by Nuxt
import type { Ref } from 'vue'
import { useStorage } from '@vueuse/core'
import { useAsyncData, useNuxtApp, useState } from '#app'
import type { router } from '~/server/trpc'
type MaybeRef<T> = T | Ref<T>
type AppRouter = typeof router
type inferProcedures<
export type inferProcedures<
TObj extends ProcedureRecord<any, any, any, any, any, any>,
> = {
[TPath in keyof TObj]: {
@@ -26,10 +26,20 @@ type inferProcedures<
};
}
type TQueries = AppRouter['_def']['queries']
type TError = TRPCClientErrorLike<AppRouter>
export type TQueries = AppRouter['_def']['queries']
export type TError = TRPCClientErrorLike<AppRouter>
type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
export type TQueryValues = inferProcedures<AppRouter['_def']['queries']>
/**
* Calculates the key used for `useAsyncData` call
* @param pathAndInput
*/
export function getQueryKey<
TPath extends keyof TQueryValues & string,
>(pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>]) {
return `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
}
export async function useAsyncQuery<
TPath extends keyof TQueryValues & string,
@@ -40,16 +50,16 @@ export async function useAsyncQuery<
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
options: AsyncDataOptions<TOutput, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TError>> {
const client = useClient()
const key = `${pathAndInput[0]}-${objectHash(pathAndInput[1] ? JSON.stringify(pathAndInput[1]) : '')}`
const { $client } = useNuxtApp()
const key = getQueryKey(pathAndInput)
const serverError = useState<TError | null>(`error-${key}`, () => null)
const { error, data, ...rest } = await useAsyncData(
key,
() => client.query(...pathAndInput),
() => $client.query(...pathAndInput),
// @ts-expect-error: Internal
options,
)
// @ts-expect-error: Resolved by Nuxt
if (process.server && error.value && !serverError.value)
serverError.value = error.value as any
@@ -62,3 +72,12 @@ export async function useAsyncQuery<
error: serverError,
} as any
}
export function useClient(): TRPCClient<AppRouter> {
const { $client } = useNuxtApp()
return $client
}
export function useClientHeaders(initialValue: MaybeRef<Record<string, any>> = {}): Ref<Record<string, any>> {
return useStorage('trpc-nuxt-header', initialValue)
}

30
src/runtime/plugin.ts Normal file
View File

@@ -0,0 +1,30 @@
import * as trpc from '@trpc/client'
import { unref } from 'vue'
import { useClientHeaders } from './client'
import { defineNuxtPlugin, useRequestHeaders, useRuntimeConfig } from '#app'
import type { router } from '~/server/trpc'
declare type AppRouter = typeof router
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig().public.trpc
const headers = useRequestHeaders()
const otherHeaders = useClientHeaders()
const client = trpc.createTRPCClient<AppRouter>({
url: `${config.baseURL}${config.endpoint}`,
headers: () => {
return {
...unref(otherHeaders),
...headers,
}
},
})
nuxtApp.provide('client', client)
})
declare module '#app' {
interface NuxtApp {
$client: trpc.TRPCClient<AppRouter>
}
}