Compare commits

...

97 Commits

Author SHA1 Message Date
Robert Soriano
217c3fd8e9 chore: release v0.4.0-beta.0 2022-10-29 22:51:51 -07:00
Robert Soriano
14617d864d fix build 2022-10-29 22:48:13 -07:00
Robert Soriano
3522cc9327 move to pnpm workspace 2022-10-29 22:45:57 -07:00
Robert Soriano
09500bc868 update readme 2022-10-29 22:39:43 -07:00
Robert Soriano
6ddfbf9629 update playground 2022-10-29 22:38:10 -07:00
Robert Soriano
97dfefe6cd add superjson to playground 2022-10-29 22:33:59 -07:00
Robert Soriano
b61dbd1786 update playground 2022-10-29 22:29:21 -07:00
Robert Soriano
4bb2cdc2d2 save trpc server error to client 2022-10-29 22:16:06 -07:00
Robert Soriano
29909328c4 update playground 2022-10-29 21:58:00 -07:00
Robert Soriano
da44e5429f update devDeps 2022-10-29 21:51:27 -07:00
Robert Soriano
ae920a37fd make trpc-nuxt flexible by exporting server and client functions instead of nuxt modules 2022-10-29 21:50:27 -07:00
Robert Soriano
bad2de134b remove queryKey for mutation 2022-10-29 19:38:29 -07:00
Robert Soriano
14da8bbda6 update example 2022-10-29 19:31:42 -07:00
Robert Soriano
e69dacf07a import useAsyncData from #app 2022-10-29 19:26:32 -07:00
Robert Soriano
f8de361eaf add inspiration comment 2022-10-29 19:24:43 -07:00
Robert Soriano
0a5735125f move types to another file 2022-10-29 19:23:54 -07:00
Robert Soriano
a04ed72c45 update playground 2022-10-29 19:20:47 -07:00
Robert Soriano
d435c7c9c5 update export paths 2022-10-29 19:18:15 -07:00
Robert Soriano
a3ce2c9a92 remove unused module imports 2022-10-29 19:15:59 -07:00
Robert Soriano
b0e7cc1854 rename api to server 2022-10-29 19:12:44 -07:00
Robert Soriano
9d07dab57d add custom fetch function for node 18 2022-10-29 19:12:01 -07:00
Robert Soriano
7257842438 rewrite client 2022-10-29 19:02:14 -07:00
Robert Soriano
b72b0449c1 update client types 2022-10-28 15:50:54 -07:00
Robert Soriano
d47fbc4537 add exported router type to plugin 2022-10-28 15:40:31 -07:00
Robert Soriano
c0b570ca09 api support for trpc v10 2022-10-28 15:39:31 -07:00
Robert Soriano
95f5478cdc update server routes in playground 2022-10-28 15:27:50 -07:00
Robert Soriano
000ac03295 update client to use v10 client 2022-10-28 15:13:29 -07:00
Robert Soriano
91421c1849 move @trpc/client and @trpc/server to peerDependencies 2022-10-28 15:06:13 -07:00
Robert Soriano
bdeada0e6e Merge pull request #32 from littlejon85/9-server-error-discard
fix: don't discard error in client
2022-10-28 14:53:09 -07:00
Robert Soriano
fd9b86fb18 release v0.3.1 2022-10-21 12:27:16 -07:00
Robert Soriano
5fab0e54b0 update local deps 2022-10-21 12:24:18 -07:00
Robert Soriano
7d2f464699 Merge pull request #34 from mahdiboomeri/chore
chore: bump nuxt to RC 12
2022-10-21 08:10:53 -07:00
Mahdi Boomeri
fdb9c2b474 refactor: playground nuxt.config 2022-10-21 17:00:22 +03:30
Mahdi Boomeri
4ed504e1e9 fix: h3 CompatibilityEvent 2022-10-21 16:56:09 +03:30
Mahdi Boomeri
92747711c6 chore: bump other deps 2022-10-21 16:53:53 +03:30
Mahdi Boomeri
3646e8cdfe chore: nump nuxt 2022-10-21 16:43:22 +03:30
Jan Dlouhý
88214185a8 fix: use useAsyncData error in client 2022-10-15 22:36:28 +02:00
Jan Dlouhý
ad03ae7b65 fix: don't discard error in client 2022-10-11 14:35:21 +02:00
Robert Soriano
09e8ab536a feat: replace useBody with readBody 2022-10-01 09:15:36 -07:00
Robert Soriano
f7dadd482d Update README.md 2022-09-29 10:20:05 -07:00
Robert Soriano
7b69cff1ed Merge pull request #30 from cawa-93/patch-1
docs: Fix `baseURL` in code sample
2022-09-27 10:18:15 -07:00
Alex Kozack
f2d7763fa4 docs: Fix baseURL in code sample 2022-09-27 10:41:38 +03:00
Robert Soriano
20f5a08cbd update readme 2022-09-24 08:26:36 -07:00
Robert Soriano
75980c0953 release v0.3.0 2022-09-24 08:25:33 -07:00
Robert Soriano
a6f61d404f feat(deps): bump h3 to 0.7.21 2022-09-24 08:24:57 -07:00
Robert Soriano
e53d1be61e feat(deps): bump @nuxt/kit to 3.0.0-rc-11 2022-09-24 08:23:29 -07:00
Robert Soriano
d152ebc7be chore: remove volta config 2022-09-24 08:20:24 -07:00
Robert Soriano
f6daa2b7c3 Merge pull request #28 from robinWongM/feature/direct-api-call-during-ssr
feat: use globalThis.$fetch to call API handler directly on server-side
2022-09-24 08:19:16 -07:00
Robin Wong
7962c7f61e fix: return original FetchResponse when ohmyfetch throws error 2022-09-18 09:46:59 +00:00
Robin Wong
453845fb14 fix: eslint errors 2022-09-18 08:44:43 +00:00
Robin Wong
ee3ee485ba feat: use globalThis.$fetch to call API handler directly on server-side 2022-09-16 16:53:15 +00:00
Robert Soriano
cdf29bee67 release v0.2.8 2022-09-08 09:36:32 -07:00
Robert Soriano
d3d35404b1 update release script 2022-09-08 09:36:24 -07:00
Robert Soriano
e57c919157 Merge pull request #26 from elfpie/feature/update-nuxt
Bump nuxt to RC9
2022-09-08 08:11:24 -07:00
Felipe
d0fffbbc64 feat(deps): bump nuxt to 3.0.0-rc.9 2022-09-07 17:59:23 -03:00
Felipe
433d5ada46 feat(deps): bump @trpc/client to 9.27.2 2022-09-07 17:59:23 -03:00
Felipe
5d180460af feat(deps): bump @trpc/server to 9.27.2 2022-09-07 17:59:23 -03:00
Felipe
8991148d39 feat(deps): bump @nuxt/kit to 3.0.0-rc.9 2022-09-07 17:59:15 -03:00
Robert Soriano
dd05285d65 release v0.2.7 2022-08-31 12:10:12 -07:00
Robert Soriano
d058ff241a update playground 2022-08-31 12:09:45 -07:00
Robert Soriano
fb4894e264 Merge pull request #25 from mahdiboomeri/fix/useClientHeaders
Fix useClientHeaders
2022-08-31 12:07:05 -07:00
Mahdi Boomeri
186faff140 docs: replaced useStorage with useState in useClientHeaders 2022-08-31 19:19:27 +04:30
Mahdi Boomeri
b9cdb228b7 refactor: remove vueuse dependency 2022-08-31 19:17:44 +04:30
Mahdi Boomeri
1e67434a8f fix: useClientHeaders returning an empty object 2022-08-31 19:13:29 +04:30
Robert Soriano
019bb20e50 release v0.2.6 2022-08-20 00:10:00 -07:00
Robert Soriano
695ce4c56e feat(deps): bump @trpc/server from 9.27.0 to 9.27.1 2022-08-20 00:09:39 -07:00
Robert Soriano
38649b7023 feat(deps): bump @trpc/client from 9.27.0 to 9.27.1 2022-08-20 00:09:17 -07:00
Robert Soriano
5cfef32baa release v0.2.5 2022-08-14 12:38:56 -07:00
Robert Soriano
8497289c65 update readme 2022-08-14 12:38:31 -07:00
Robert Soriano
224c4b317b move ohash to deps 2022-08-14 12:29:04 -07:00
Robert Soriano
d5d6780ca8 bump ohash 2022-08-14 12:28:03 -07:00
Robert Soriano
252d0f6692 feat(deps): bump vueuse from 8.9.3 to 9.1.0 2022-08-14 12:27:10 -07:00
Robert Soriano
d022710591 bump dev deps 2022-08-14 12:26:33 -07:00
Robert Soriano
a926968f13 feat(deps): bump @nuxt/kit from 3.0.0-rc.5 to 3.0.0-rc.8 2022-08-14 12:25:48 -07:00
Robert Soriano
a8eba5ab8d feat(deps): bump @trpc/server from 9.26.0 to 9.27.0 2022-08-14 12:25:14 -07:00
Robert Soriano
5871528d67 feat(deps): bump @trpc/client from 9.26.0 to 9.27.0 2022-08-14 12:24:52 -07:00
Robert Soriano
def9d0b19d volta pin node@16 2022-08-14 12:24:20 -07:00
Robert Soriano
34f50fc4d6 chore(deps): bump pnpm to 7.5.0 2022-07-14 19:04:19 -07:00
Robert Soriano
9fe1469eba release v0.2.4 2022-07-14 19:03:09 -07:00
Robert Soriano
e48d7cd42c feat(deps): bump @nuxt/kit from 3.0.0-rc.4 to 3.0.0-rc.5 2022-07-14 19:02:54 -07:00
Robert Soriano
2b517ad77b release v0.2.3 2022-07-14 11:19:28 -07:00
Robert Soriano
77ee994e13 update deps 2022-07-14 11:19:19 -07:00
Robert Soriano
67a8a2a3ed feat(deps): bump ufo from 0.8.4 to 0.8.5 2022-07-14 11:19:01 -07:00
Robert Soriano
556b5c8e08 feat(deps): bump @vueuse/nuxt from 8.7.4 to 8.9.3 2022-07-14 11:18:37 -07:00
Robert Soriano
85154f12d7 feat(deps): bump @vueuse/core from 8.7.4 to 8.9.3 2022-07-14 11:18:21 -07:00
Robert Soriano
26ce46e12b feat(deps): bump @trpc/server from 9.25.3 to 9.26.0 2022-07-14 11:17:51 -07:00
Robert Soriano
c2cfd9a214 feat(deps): bump @trpc/client from 9.25.3 to 9.26.0 2022-07-14 11:17:34 -07:00
Robert Soriano
f736545128 ci: add release workflow 2022-07-14 11:16:43 -07:00
Robert Soriano
59a9eb5f95 fix playground workspace 2022-07-05 20:24:40 -07:00
Robert Soriano
48fc19076f Update README.md 2022-06-23 10:03:48 -07:00
Robert Soriano
6b83e5d4ea chore(deps): bump @trpc/server to 9.25.3 2022-06-18 21:06:25 -07:00
Robert Soriano
0358a258d7 chore(deps): bump @trpc/client to 9.25.3 2022-06-18 21:06:11 -07:00
Robert Soriano
8e7f33c9d2 chore(deps): bump h3 to 0.7.10 2022-06-18 21:05:48 -07:00
Robert Soriano
b8f4af2df3 chore(deps): bump @vueuse/nuxt to 8.7.4 2022-06-18 21:05:15 -07:00
Robert Soriano
666751678c chore(deps): bump @vueuse/core to 8.7.4 2022-06-18 21:04:56 -07:00
Robert Soriano
cab89aaa66 chore(deps): bump bumpp to 7.2.0 2022-06-18 21:04:17 -07:00
Robert Soriano
4c9850fafc chore(deps): add @types/dedent 2022-06-18 21:02:44 -07:00
46 changed files with 2686 additions and 3479 deletions

22
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npx changelogithub
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

171
README.md
View File

@@ -1,171 +0,0 @@
# tRPC-Nuxt
[![Version](https://img.shields.io/npm/v/trpc-nuxt?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/trpc-nuxt)
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
npm i trpc-nuxt
```
```ts
// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
modules: ['trpc-nuxt'],
trpc: {
baseURL: 'http://localhost:3000', // defaults to http://localhost:3000
endpoint: '/trpc', // defaults to /trpc
},
typescript: {
strict: true // required to make input/output types work
}
})
```
## Usage
Expose your tRPC [routes](https://trpc.io/docs/router) under `~/server/trpc/index.ts`:
```ts
// ~/server/trpc/index.ts
import type { inferAsyncReturnType } from '@trpc/server'
import * as trpc from '@trpc/server'
import { z } from 'zod' // yup/superstruct/zod/myzod/custom
export const router = trpc.router()
// queries and mutations...
.query('getUsers', {
async resolve(req) {
// use your ORM of choice
return await UserModel.all()
},
})
.mutation('createUser', {
// validate input with Zod
input: z.object({ name: z.string().min(5) }),
async resolve(req) {
// use your ORM of choice
return await UserModel.create({
data: req.input,
})
},
})
```
Use the client like so:
```ts
const client = useClient() // auto-imported
const users = await client.query('getUsers')
const newUser = await client.mutation('createUser', {
name: 'wagmi'
})
```
## useAsyncQuery
A thin wrapper around [`useAsyncData`](https://v3.nuxtjs.org/api/composables/use-async-data/) and `client.query()`.
The first argument is a `[path, input]`-tuple - if the `input` is optional, you can omit the, `input`-part.
You'll notice that you get autocompletion on the `path` and automatic typesafety on the `input`.
```ts
const {
data,
pending,
error,
refresh
} = await useAsyncQuery(['getUser', { id: 69 }], {
// pass useAsyncData options here
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`:
```ts
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import type { CompatibilityEvent } from 'h3'
import type { OnErrorPayload } from 'trpc-nuxt/api'
export const router = trpc.router<inferAsyncReturnType<typeof createContext>>()
// Optional
// https://trpc.io/docs/context
export const createContext = (event: CompatibilityEvent) => {
// ...
return {
/** context data */
}
}
// Optional
// https://trpc.io/docs/caching#using-responsemeta--to-cache-responses
export const responseMeta = () => {
// ...
return {
// { headers: ... }
}
}
// Optional
// https://trpc.io/docs/error-handling#handling-errors
export const onError = (payload: OnErrorPayload<typeof router>) => {
// Do whatever here like send to bug reporting and stuff
}
```
## Recipes
- [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

1
api.d.ts vendored
View File

@@ -1 +0,0 @@
export * from './dist/runtime/api'

View File

@@ -1,59 +1,15 @@
{
"name": "trpc-nuxt",
"type": "module",
"version": "0.2.2",
"packageManager": "pnpm@7.1.1",
"license": "MIT",
"main": "./dist/module.cjs",
"types": "./dist/types.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": "./dist/module.mjs",
"require": "./dist/module.cjs"
},
"./api": {
"import": "./dist/runtime/api.mjs",
"types": "./dist/runtime/api.d.ts"
}
},
"files": [
"dist",
"*.d.ts"
],
"name": "trpc-nuxt-workspace",
"packageManager": "pnpm@7.5.0",
"scripts": {
"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",
"prepare": "nuxi prepare playground"
},
"dependencies": {
"@nuxt/kit": "^3.0.0-rc.4",
"@trpc/client": "^9.25.2",
"@trpc/server": "^9.25.2",
"@vueuse/core": "^8.6.0",
"@vueuse/nuxt": "^8.6.0",
"dedent": "^0.7.0",
"defu": "^6.0.0",
"h3": "^0.7.9",
"pathe": "^0.3.0",
"ufo": "^0.8.4"
"lint:fix": "eslint . --fix"
},
"devDependencies": {
"@antfu/eslint-config": "^0.23.1",
"@antfu/ni": "^0.16.2",
"@nuxt/module-builder": "latest",
"bumpp": "^7.1.1",
"eslint": "^8.14.0",
"nuxt": "3.0.0-rc.4",
"ohash": "^0.1.0",
"pnpm": "^7.1.1",
"trpc-nuxt": "workspace:*",
"zod": "^3.16.0"
"@antfu/eslint-config": "^0.27.0",
"@antfu/ni": "^0.18.3",
"eslint": "^8.25.0",
"pnpm": "^7.5.0"
},
"eslintConfig": {
"extends": "@antfu",

View File

@@ -1,11 +1,7 @@
import { defineNuxtConfig } from 'nuxt'
import Module from '../src/module'
// https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({
modules: [Module],
runtimeConfig: {
baseURL: 'http://localhost:3000',
baseURL: '',
},
typescript: {
strict: true,

View File

@@ -0,0 +1,19 @@
{
"name": "playground",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"superjson": "^1.11.0",
"trpc-nuxt": "workspace:*",
"zod": "^3.19.1"
},
"devDependencies": {
"nuxt": "3.0.0-rc.12"
}
}

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
const { $client } = useNuxtApp()
// const headers = useClientHeaders()
// const addHeader = () => {
// headers.value.authorization = 'Bearer abcdefghijklmnop'
// console.log(headers.value)
// }
const addTodo = async () => {
const title = Math.random().toString(36).slice(2, 7)
try {
const result = await $client.todo.addTodo.mutate({
id: Date.now(),
userId: 69,
title,
completed: false,
})
await result.execute()
console.log('Todo: ', result.data.value)
}
catch (e) {
console.log(e)
}
}
const { data: todos, pending, error, refresh } = await $client.todo.getTodos.query()
</script>
<template>
<div>
<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>
</div>
</template>
<style>
a {
text-decoration: none;
}
.completed {
text-decoration: line-through;
}
</style>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
const route = useRoute()
const { $client } = useNuxtApp()
const { data: todo, pending, error } = await $client.todo.getTodo.query(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

@@ -0,0 +1,50 @@
import { httpBatchLink, loggerLink } from '@trpc/client'
import { createTRPCNuxtProxyClient } from 'trpc-nuxt/client'
import superjson from 'superjson'
import type { AppRouter } from '~~/server/trpc/routers'
export default defineNuxtPlugin((nuxtApp) => {
const client = createTRPCNuxtProxyClient<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: 'http://localhost:3000/api/trpc',
/**
* Set custom request headers on every request from tRPC
* @link https://trpc.io/docs/ssr
*/
headers() {
if (nuxtApp.ssrContext?.event?.req) {
// To use SSR properly, you need to forward the client's headers to the server
// This is so you can pass through things like cookies when we're server-side rendering
// If you're using Node 18, omit the "connection" header
const {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
connection: _connection,
...headers
} = nuxtApp.ssrContext.event.req.headers
return {
...headers,
// Optional: inform server that it's an SSR request
'x-ssr': '1',
}
}
return {}
},
}),
],
})
return {
provide: {
client,
},
}
})

View File

@@ -0,0 +1,23 @@
import { createNuxtApiHandler } from 'trpc-nuxt'
import { appRouter } from '../../trpc/routers'
import { createContext } from '~~/server/trpc/context'
export default createNuxtApiHandler({
router: appRouter,
/**
* @link https://trpc.io/docs/context
*/
createContext,
onError({ error }) {
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
console.error('Something went wrong', error)
}
},
/**
* @link https://trpc.io/docs/caching#api-response-caching
*/
// responseMeta() {
// // ...
// },
})

View File

@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type { inferAsyncReturnType } from '@trpc/server'
import type { H3Event } from 'h3'
export type Context = inferAsyncReturnType<typeof createContext>
/**
* Creates context for an incoming request
* @link https://trpc.io/docs/context
*/
export async function createContext(
opts: H3Event,
) {
// for API-response caching see https://trpc.io/docs/caching
return {}
}

View File

@@ -0,0 +1,29 @@
import { initTRPC } from '@trpc/server'
import superjson from 'superjson'
import type { Context } from './context'
const t = initTRPC.context<Context>().create({
transformer: superjson,
})
/**
* Create a router
* @see https://trpc.io/docs/v10/router
*/
export const router = t.router
/**
* Create an unprotected procedure
* @see https://trpc.io/docs/v10/procedures
**/
export const publicProcedure = t.procedure
/**
* @see https://trpc.io/docs/v10/middlewares
*/
export const middleware = t.middleware
/**
* @see https://trpc.io/docs/v10/merging-routers
*/
export const mergeRouters = t.mergeRouters

View File

@@ -0,0 +1,10 @@
import { router } from '..'
import { todoRouter } from './todo'
import { userRouter } from './user'
export const appRouter = router({
todo: todoRouter,
user: userRouter,
})
export type AppRouter = typeof appRouter

View File

@@ -0,0 +1,33 @@
import { z } from 'zod'
import { publicProcedure, router } from '..'
const baseURL = 'https://jsonplaceholder.typicode.com'
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 todoRouter = router({
getTodos: publicProcedure
.query(() => {
return $fetch<Todo[]>(`${baseURL}/todos`)
}),
getTodo: publicProcedure
.input(z.number())
.query((req) => {
return $fetch<Todo>(`${baseURL}/todos/${req.input}`)
}),
addTodo: publicProcedure
.input(TodoShape)
.mutation((req) => {
return $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
}),
})

View File

@@ -0,0 +1,33 @@
import { z } from 'zod'
import { publicProcedure, router } from '..'
const baseURL = 'https://jsonplaceholder.typicode.com'
const UserShape = z.object({
id: z.number(),
name: z.string(),
username: z.string(),
email: z.string(),
})
export type User = z.infer<typeof UserShape>
export const userRouter = router({
getUsers: publicProcedure
.query(() => {
return $fetch<User[]>(`${baseURL}/users`)
}),
getUser: publicProcedure
.input(z.number())
.query((req) => {
return $fetch<User>(`${baseURL}/users/${req.input}`)
}),
addUser: publicProcedure
.input(UserShape)
.mutation((req) => {
return $fetch<User>(`${baseURL}/users`, {
method: 'POST',
body: req.input,
})
}),
})

View File

@@ -0,0 +1,4 @@
{
// https://v3.nuxtjs.org/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

View File

@@ -0,0 +1,15 @@
# tRPC-Nuxt
[![Version](https://img.shields.io/npm/v/trpc-nuxt?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/trpc-nuxt)
End-to-end typesafe APIs with [tRPC.io](https://trpc.io/) in Nuxt applications.
Learn more about tRPC.io [here](https://trpc.io/docs/v10).
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## License
MIT

1
packages/trpc-nuxt/client.d.ts vendored Normal file
View File

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

View File

@@ -0,0 +1,58 @@
{
"name": "trpc-nuxt",
"version": "0.4.0-beta.0",
"packageManager": "pnpm@7.5.0",
"license": "MIT",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./client": {
"types": "./dist/client.d.ts",
"require": "./dist/client.js",
"import": "./dist/client.mjs"
}
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist",
"*.d.ts"
],
"scripts": {
"prepublishOnly": "nr build",
"build": "tsup",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp --commit --push --tag && npm publish"
},
"peerDependencies": {
"@trpc/client": "^10.0.0-proxy-beta.21",
"@trpc/server": "^10.0.0-proxy-beta.21",
"nuxt": "^3.0.0-rc.12"
},
"dependencies": {
"h3": "^0.8.5",
"ohash": "^0.1.5",
"ohmyfetch": "^0.4.20",
"ufo": "^0.8.6"
},
"devDependencies": {
"@trpc/client": "10.0.0-rc.1",
"@trpc/server": "10.0.0-rc.1",
"bumpp": "^8.2.1",
"nuxt": "3.0.0-rc.12",
"tsup": "^6.3.0",
"typescript": "4.5.4"
},
"eslintConfig": {
"extends": "@antfu",
"rules": {
"no-console": "warn"
}
}
}

View File

@@ -0,0 +1,95 @@
import type { CreateTRPCClientOptions, inferRouterProxyClient } from '@trpc/client'
import { createTRPCProxyClient } from '@trpc/client'
import { FetchError } from 'ohmyfetch'
import type {
AnyRouter,
} from '@trpc/server'
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
import { hash } from 'ohash'
import type { DecoratedProcedureRecord } from './types'
/**
* Calculates the key used for `useAsyncData` call
*/
export function getQueryKey(
path: string,
input: unknown,
): string {
return input === undefined ? path : `${path}-${hash(input || '')}`
}
/**
* @internal
*/
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 `.mutate` or `.query()`
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const lastArg = pathCopy.pop()!
const path = pathCopy.join('.')
const [input, asyncDataOptions] = args
const queryKey = getQueryKey(path, input)
if (lastArg === 'mutate') {
return useAsyncDataWithError(queryKey, () => (client as any)[path][lastArg](input), {
...asyncDataOptions as Record<string, any>,
immediate: false,
})
}
return useAsyncDataWithError(queryKey, () => (client as any)[path][lastArg](input), asyncDataOptions)
})
}
/**
* Custom useAsyncData to add server error to client
*/
async function useAsyncDataWithError(queryKey: string, cb: any, asyncDataOptions: any) {
// @ts-ignore: nuxt internal
const serverError = useState(`error-${queryKey}`, () => null)
// @ts-ignore: nuxt internal
const { error, data, ...rest } = await useAsyncData(queryKey, cb, asyncDataOptions)
if (error.value && !serverError.value)
serverError.value = error.value as any
if (data.value)
serverError.value = null
return {
...rest,
data,
error: serverError,
}
}
export function createTRPCNuxtProxyClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>) {
const client = createTRPCProxyClient(opts)
const decoratedClient = createFlatProxy((key) => {
return createNuxtProxyDecoration(key, client)
}) as DecoratedProcedureRecord<TRouter['_def']['record']>
return decoratedClient
}
export function customFetch(input: RequestInfo | URL, options?: RequestInit) {
return 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),
}))
}

View File

@@ -0,0 +1 @@
export * from './server'

View File

@@ -1,20 +1,20 @@
import { resolveHTTPResponse } from '@trpc/server'
import type { ResponseMeta } from '@trpc/server/http'
import { resolveHTTPResponse } from '@trpc/server/http'
import type {
AnyRouter,
ProcedureType,
ResponseMeta,
TRPCError,
inferRouterContext,
inferRouterError,
} from '@trpc/server'
import { createURL } from 'ufo'
import type { CompatibilityEvent } from 'h3'
import { defineEventHandler, isMethod, useBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/dist/declarations/src/rpc'
import type { H3Event } from 'h3'
import { defineEventHandler, isMethod, readBody } from 'h3'
import type { TRPCResponse } from '@trpc/server/rpc'
type MaybePromise<T> = T | Promise<T>
export type CreateContextFn<TRouter extends AnyRouter> = (event: CompatibilityEvent) => MaybePromise<inferRouterContext<TRouter>>
export type CreateContextFn<TRouter extends AnyRouter> = (event: H3Event) => MaybePromise<inferRouterContext<TRouter>>
export interface ResponseMetaFnPayload<TRouter extends AnyRouter> {
data: TRPCResponse<unknown, inferRouterError<TRouter>>[]
@@ -30,25 +30,25 @@ export interface OnErrorPayload<TRouter extends AnyRouter> {
error: TRPCError
type: ProcedureType | 'unknown'
path: string | undefined
req: CompatibilityEvent['req']
req: H3Event['req']
input: unknown
ctx: undefined | inferRouterContext<TRouter>
}
export type OnErrorFn<TRouter extends AnyRouter> = (opts: OnErrorPayload<TRouter>) => void
export function createTRPCHandler<Router extends AnyRouter>({
export function createNuxtApiHandler<TRouter extends AnyRouter>({
router,
createContext,
responseMeta,
onError,
endpoint,
url = '/api/trpc',
}: {
router: Router
createContext?: CreateContextFn<Router>
responseMeta?: ResponseMetaFn<Router>
onError?: OnErrorFn<Router>
endpoint: string
router: TRouter
createContext?: CreateContextFn<TRouter>
responseMeta?: ResponseMetaFn<TRouter>
onError?: OnErrorFn<TRouter>
url?: string
}) {
return defineEventHandler(async (event) => {
const {
@@ -63,10 +63,10 @@ export function createTRPCHandler<Router extends AnyRouter>({
req: {
method: req.method!,
headers: req.headers,
body: isMethod(event, 'GET') ? null : await useBody(event),
body: isMethod(event, 'GET') ? null : await readBody(event),
query: $url.searchParams,
},
path: $url.pathname.substring(endpoint.length + 1),
path: $url.pathname.substring(url.length + 1),
createContext: async () => createContext?.(event),
responseMeta,
onError: (o) => {

View File

@@ -0,0 +1,64 @@
import type { TRPCClientErrorLike } from '@trpc/client'
import type {
AnyMutationProcedure,
AnyProcedure,
AnyQueryProcedure,
AnyRouter,
ProcedureRouterRecord,
inferProcedureInput,
inferProcedureOutput,
} from '@trpc/server'
import type {
AsyncData,
AsyncDataOptions,
KeyOfRes,
PickFrom,
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
// Inspired by trpc/react-query client types
// https://github.com/trpc/trpc/blob/next/packages/react-query/src/createTRPCReact.tsx
/**
* @internal
*/
export type DecorateProcedure<
TProcedure extends AnyProcedure,
TPath extends string,
> = TProcedure extends AnyQueryProcedure
? {
query: <
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys>,
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : TProcedure extends AnyMutationProcedure ? {
mutate: <
TData = inferProcedureOutput<TProcedure>,
Transform extends _Transform<TData> = _Transform<TData, TData>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
input: inferProcedureInput<TProcedure>,
opts?: AsyncDataOptions<TData, Transform, PickKeys>,
) => AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TRPCClientErrorLike<TProcedure>>
} : never
/**
* @internal
*/
export type DecoratedProcedureRecord<
TProcedures extends ProcedureRouterRecord,
TPath extends string = '',
> = {
[TKey in keyof TProcedures]: TProcedures[TKey] extends AnyRouter
? DecoratedProcedureRecord<
TProcedures[TKey]['_def']['record'],
`${TPath}${TKey & string}.`
>
: TProcedures[TKey] extends AnyProcedure
? DecorateProcedure<TProcedures[TKey], `${TPath}${TKey & string}`>
: never;
}

View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"module": "esnext",
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"skipLibCheck": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"allowJs": true,
"noEmit": true,
"outDir": "dist",
"resolveJsonModule": true
}
}

View File

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

View File

@@ -1,4 +0,0 @@
{
"name": "playground",
"private": true
}

View File

@@ -1,19 +0,0 @@
<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

@@ -1,64 +0,0 @@
<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

@@ -1,18 +0,0 @@
<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,53 +0,0 @@
import * as trpc from '@trpc/server'
import type { inferAsyncReturnType } from '@trpc/server'
import { z } from 'zod'
import type { CompatibilityEvent } from 'h3'
const baseURL = 'https://jsonplaceholder.typicode.com'
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('getTodo', {
input: z.number(),
async resolve(req) {
return await $fetch<Todo>(`${baseURL}/todos/${req.input}`)
},
})
.mutation('addTodo', {
input: TodoShape,
async resolve(req) {
console.log(req.input)
return await $fetch<Todo>(`${baseURL}/todos`, {
method: 'POST',
body: req.input,
})
},
})
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 {
}
}
type Context = inferAsyncReturnType<typeof createContext>

4634
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +1,2 @@
packages:
- playground/*
- packages/*

View File

@@ -1,102 +0,0 @@
## Authorization
The `createContext`-function is called for each incoming request so here you can add contextual information about the calling user from the request object.
### Create context from request headers
```ts
// ~/server/trpc/index.ts
import type { inferAsyncReturnType } from '@trpc/server'
import type { CompatibilityEvent } from 'h3'
import { decodeAndVerifyJwtToken } from '~/somewhere/in/your/app/utils'
// The app's context - is generated for each incoming request
export async function createContext({ req }: 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
async function getUserFromHeader() {
if (req.headers.authorization) {
const user = await decodeAndVerifyJwtToken(req.headers.authorization.split(' ')[1])
return user
}
return null
}
const user = await getUserFromHeader()
return {
user,
}
}
type Context = inferAsyncReturnType<typeof createContext>
// [..] Define API handler and app router
```
### Option 1: Authorize using resolver
```ts
import { TRPCError } from '@trpc/server'
export const router = trpc
.router<Context>()
// open for anyone
.query('hello', {
input: z.string().nullish(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`
},
})
// checked in resolver
.query('secret', {
resolve: ({ ctx }) => {
if (!ctx.user)
throw new TRPCError({ code: 'UNAUTHORIZED' })
return {
secret: 'sauce',
}
},
})
```
### Option 2: Authorize using middleware
```ts
import * as trpc from '@trpc/server'
import { TRPCError } from '@trpc/server'
// Merging routers: https://trpc.io/docs/merging-routers
export const router = trpc
.router<Context>()
// this is accessible for everyone
.query('hello', {
input: z.string().nullish(),
resolve: ({ input, ctx }) => {
return `hello ${input ?? ctx.user?.name ?? 'world'}`
},
})
.merge(
'admin.',
trpc.router<Context>()
// this protects all procedures defined next in this router
.middleware(async ({ ctx, next }) => {
if (!ctx.user?.isAdmin)
throw new TRPCError({ code: 'UNAUTHORIZED' })
return next()
})
.query('secret', {
resolve: ({ ctx }) => {
return {
secret: 'sauce',
}
},
}),
)
```
Learn more about authorization [here](https://trpc.io/docs/authorization).

View File

@@ -1,41 +0,0 @@
## Error Formatting
The error formatting in your router will be inferred all the way to your client (& Vue components).
### Adding custom formatting
```ts
// ~/server/trpc/index.ts
import * as trpc from '@trpc/server'
export const router = trpc.router<Context>()
.formatError(({ shape, error }) => {
return {
...shape,
data: {
...shape.data,
zodError:
error.code === 'BAD_REQUEST'
&& error.cause instanceof ZodError
? error.cause.flatten()
: null,
}
}
})
```
### Usage in Vue
```html
<script setup lang="ts">
const { error } = await useAsyncQuery(['getUser', { id: 69 }])
</script>
<template>
<pre v-if="error?.data?.zodError">
{{ JSON.stringify(error.data.zodError, null, 2) }}
</pre>
</template>
```
Learn more about error formatting [here](https://trpc.io/docs/error-formatting).

View File

@@ -1,15 +0,0 @@
## Handling errors
All errors that occur in a procedure go through the `onError` method before being sent to the client. Here you can handle or change errors.
```ts
// ~/server/trpc/index.ts
import * as trpc from '@trpc/server'
export function onError({ error, type, path, input, ctx, req }) {
console.error('Error:', error)
if (error.code === 'INTERNAL_SERVER_ERROR') {
// send to bug reporting
}
}
```

View File

@@ -1,80 +0,0 @@
## 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

@@ -1,46 +0,0 @@
# 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,49 +0,0 @@
## Validation
tRPC works out-of-the-box with yup/superstruct/zod/myzod/custom validators.
### Input Validation
```ts
// ~/server/trpc/index.ts
import { z } from 'zod'
export const router = trpc
.router()
.mutation('createUser', {
// validate input with Zod
input: z.object({
name: z.string().min(5)
}),
async resolve(req) {
// use your ORM of choice
return await UserModel.create({
data: req.input,
})
},
})
```
### Output Validation
```ts
// ~/server/trpc/index.ts
import { z } from 'zod'
export const router = trpc
.router()
.query('hello', {
// validate output with Zod
output: z.object({
greeting: z.string()
}),
// expects return type of { greeting: string }
resolve() {
return {
greeting: 'hello!',
}
},
})
```
Learn more about input validation [here](https://trpc.io/docs/router#input-validation).

View File

@@ -1,70 +0,0 @@
import { fileURLToPath } from 'url'
import { join, resolve } from 'pathe'
import { defu } from 'defu'
// @ts-expect-error: No types
import dedent from 'dedent'
import { addAutoImport, addPlugin, addServerHandler, addTemplate, defineNuxtModule } from '@nuxt/kit'
export interface ModuleOptions {
baseURL: string
endpoint: string
}
export default defineNuxtModule<ModuleOptions>({
meta: {
name: 'trpc-nuxt',
configKey: 'trpc',
},
defaults: {
baseURL: 'http://localhost:3000',
endpoint: '/trpc',
},
async setup(options, nuxt) {
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
nuxt.options.build.transpile.push(runtimeDir, '#build/trpc-handler')
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,
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.endpoint}/*`,
handler: handlerPath,
})
addPlugin(resolve(runtimeDir, 'plugin'))
addTemplate({
filename: 'trpc-handler.ts',
write: true,
getContents() {
return dedent`
import { createTRPCHandler } from 'trpc-nuxt/api'
import * as functions from '${trpcOptionsPath}'
export default createTRPCHandler({
...functions,
endpoint: '${finalConfig.endpoint}'
})
`
},
})
},
})

View File

@@ -1,83 +0,0 @@
import type {
AsyncData,
AsyncDataOptions,
KeyOfRes,
PickFrom,
_Transform,
} from 'nuxt/dist/app/composables/asyncData'
import type { ProcedureRecord, inferHandlerInput, inferProcedureInput, inferProcedureOutput } from '@trpc/server'
import type { TRPCClient, TRPCClientErrorLike } from '@trpc/client'
import { objectHash } from 'ohash'
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
export type inferProcedures<
TObj extends ProcedureRecord<any, any, any, any, any, any>,
> = {
[TPath in keyof TObj]: {
input: inferProcedureInput<TObj[TPath]>
output: inferProcedureOutput<TObj[TPath]>
};
}
export type TQueries = AppRouter['_def']['queries']
export type TError = TRPCClientErrorLike<AppRouter>
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,
TOutput extends TQueryValues[TPath]['output'] = TQueryValues[TPath]['output'],
Transform extends _Transform<TOutput> = _Transform<TOutput, TOutput>,
PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>,
>(
pathAndInput: [path: TPath, ...args: inferHandlerInput<TQueries[TPath]>],
options: AsyncDataOptions<TOutput, Transform, PickKeys> = {},
): Promise<AsyncData<PickFrom<ReturnType<Transform>, PickKeys>, TError>> {
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),
// @ts-expect-error: Internal
options,
)
if (process.server && error.value && !serverError.value)
serverError.value = error.value as any
if (data.value)
serverError.value = null
return {
...rest,
data,
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)
}

View File

@@ -1,30 +0,0 @@
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>
}
}

View File

@@ -1,3 +0,0 @@
{
"extends": "./playground/.nuxt/tsconfig.json"
}