mirror of
https://github.com/ArthurDanjou/trpc-nuxt.git
synced 2026-01-14 12:14:40 +01:00
Merge pull request #54 from wobsoriano/with-client
Bringing back tRPC Client
This commit is contained in:
1
client.d.ts
vendored
Normal file
1
client.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './dist/client/index'
|
||||||
@@ -21,6 +21,14 @@ yarn add @trpc/server @trpc/client trpc-nuxt zod
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
```ts [nuxt.config.ts]
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
build: {
|
||||||
|
transpile: ['trpc-nuxt/client']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
#### Why @trpc/server?
|
#### Why @trpc/server?
|
||||||
|
|
||||||
For implementing tRPC endpoints and routers.
|
For implementing tRPC endpoints and routers.
|
||||||
|
|||||||
118
docs/content/1.get-started/2.usage/1.simple.md
Normal file
118
docs/content/1.get-started/2.usage/1.simple.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
title: Simple
|
||||||
|
description: tRPC-Nuxt provides first class integration with tRPC.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Simple Usage
|
||||||
|
|
||||||
|
## 1. Create a tRPC router
|
||||||
|
|
||||||
|
Initialize your tRPC backend using the `initTRPC` function and create your first router.
|
||||||
|
|
||||||
|
::code-group
|
||||||
|
|
||||||
|
```ts [server/trpc/trpc.ts]
|
||||||
|
/**
|
||||||
|
* This is your entry point to setup the root configuration for tRPC on the server.
|
||||||
|
* - `initTRPC` should only be used once per app.
|
||||||
|
* - We export only the functionality that we use so we can enforce which base procedures should be used
|
||||||
|
*
|
||||||
|
* Learn how to create protected base procedures and other things below:
|
||||||
|
* @see https://trpc.io/docs/v10/router
|
||||||
|
* @see https://trpc.io/docs/v10/procedures
|
||||||
|
*/
|
||||||
|
import { initTRPC } from '@trpc/server'
|
||||||
|
|
||||||
|
const t = initTRPC.create()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unprotected procedure
|
||||||
|
**/
|
||||||
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
|
export const router = t.router;
|
||||||
|
export const middleware = t.middleware;
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts [server/api/trpc/[trpc].ts]
|
||||||
|
/**
|
||||||
|
* This is the API-handler of your app that contains all your API routes.
|
||||||
|
* On a bigger app, you will probably want to split this file up into multiple files.
|
||||||
|
*/
|
||||||
|
import { createNuxtApiHandler } from 'trpc-nuxt'
|
||||||
|
import { publicProcedure, router } from '~/server/trpc/trpc'
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const appRouter = router({
|
||||||
|
hello: publicProcedure
|
||||||
|
// This is the input schema of your procedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
text: z.string().nullish(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(({ input }) => {
|
||||||
|
// This is what you're returning to your client
|
||||||
|
return {
|
||||||
|
greeting: `hello ${input?.text ?? 'world'}`,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
// export only the type definition of the API
|
||||||
|
// None of the actual implementation is exposed to the client
|
||||||
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
||||||
|
// export API handler
|
||||||
|
export default createNuxtApiHandler({
|
||||||
|
router: appRouter,
|
||||||
|
createContext: () => ({}),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
## 2. Create tRPC client plugin
|
||||||
|
|
||||||
|
Create a strongly-typed plugin using your API's type signature.
|
||||||
|
|
||||||
|
```ts [plugins/client.ts]
|
||||||
|
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
|
||||||
|
import type { AppRouter } from '~/server/trpc/routers'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
/**
|
||||||
|
* createTRPCNuxtClient adds a `useQuery` composable
|
||||||
|
* built on top of `useAsyncData`.
|
||||||
|
*/
|
||||||
|
const client = createTRPCNuxtClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: '/api/trpc',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
client,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Make an API request
|
||||||
|
|
||||||
|
```vue [pages/index.vue]
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $client } = useNuxtApp()
|
||||||
|
|
||||||
|
const hello = await $client.hello.useQuery({ text: 'client' })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p>{{ hello.data?.greeting }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
title: Usage
|
title: Recommended
|
||||||
description: tRPC-Nuxt provides first class integration with tRPC.
|
description: tRPC-Nuxt provides first class integration with tRPC.
|
||||||
---
|
---
|
||||||
|
|
||||||
# Usage
|
# Recommended Usage
|
||||||
|
|
||||||
## Recommended file structure
|
Recommended but not enforced file structure.
|
||||||
|
|
||||||
Recommended but not enforced file structure. This is what you get when starting from [the examples](../main/example-apps.md).
|
|
||||||
|
|
||||||
```graphql
|
```graphql
|
||||||
.
|
.
|
||||||
@@ -24,7 +22,7 @@ Recommended but not enforced file structure. This is what you get when starting
|
|||||||
│ │ ├── context.ts # <-- create app context
|
│ │ ├── context.ts # <-- create app context
|
||||||
│ │ └── trpc.ts # <-- procedure helpers
|
│ │ └── trpc.ts # <-- procedure helpers
|
||||||
├── plugins
|
├── plugins
|
||||||
│ ├── client.ts # <-- tRPC Client as a plugin
|
│ ├── client.ts # <-- tRPC client plugin
|
||||||
└── [..]
|
└── [..]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -35,17 +33,26 @@ Initialize your tRPC backend using the `initTRPC` function and create your first
|
|||||||
::code-group
|
::code-group
|
||||||
|
|
||||||
```ts [server/trpc/trpc.ts]
|
```ts [server/trpc/trpc.ts]
|
||||||
|
/**
|
||||||
|
* This is your entry point to setup the root configuration for tRPC on the server.
|
||||||
|
* - `initTRPC` should only be used once per app.
|
||||||
|
* - We export only the functionality that we use so we can enforce which base procedures should be used
|
||||||
|
*
|
||||||
|
* Learn how to create protected base procedures and other things below:
|
||||||
|
* @see https://trpc.io/docs/v10/router
|
||||||
|
* @see https://trpc.io/docs/v10/procedures
|
||||||
|
*/
|
||||||
import { initTRPC } from '@trpc/server'
|
import { initTRPC } from '@trpc/server'
|
||||||
import { Context } from '@/server/trpc/context'
|
import { Context } from '~/server/trpc/context'
|
||||||
|
|
||||||
// Avoid exporting the entire t-object since it's not very
|
|
||||||
// descriptive and can be confusing to newcomers used to t
|
|
||||||
// meaning translation in i18n libraries.
|
|
||||||
const t = initTRPC.context<Context>().create()
|
const t = initTRPC.context<Context>().create()
|
||||||
|
|
||||||
// Base router and procedure helpers
|
/**
|
||||||
export const router = t.router
|
* Unprotected procedure
|
||||||
export const publicProcedure = t.procedure
|
**/
|
||||||
|
export const publicProcedure = t.procedure;
|
||||||
|
export const router = t.router;
|
||||||
|
export const middleware = t.middleware;
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts [server/trpc/routers/index.ts]
|
```ts [server/trpc/routers/index.ts]
|
||||||
@@ -72,8 +79,8 @@ export type AppRouter = typeof appRouter
|
|||||||
|
|
||||||
```ts [server/api/trpc/[trpc].ts]
|
```ts [server/api/trpc/[trpc].ts]
|
||||||
import { createNuxtApiHandler } from 'trpc-nuxt'
|
import { createNuxtApiHandler } from 'trpc-nuxt'
|
||||||
import { appRouter } from '@/server/trpc/routers'
|
import { appRouter } from '~/server/trpc/routers'
|
||||||
import { createContext } from '@/server/trpc/context'
|
import { createContext } from '~/server/trpc/context'
|
||||||
|
|
||||||
// export API handler
|
// export API handler
|
||||||
export default createNuxtApiHandler({
|
export default createNuxtApiHandler({
|
||||||
@@ -105,39 +112,18 @@ If you need to split your router into several subrouters, you can implement them
|
|||||||
Create a strongly-typed plugin using your API's type signature.
|
Create a strongly-typed plugin using your API's type signature.
|
||||||
|
|
||||||
```ts [plugins/client.ts]
|
```ts [plugins/client.ts]
|
||||||
import { httpBatchLink, createTRPCProxyClient } from '@trpc/client'
|
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
|
||||||
import type { AppRouter } from '@/server/trpc/routers'
|
import type { AppRouter } from '~/server/trpc/routers'
|
||||||
import { FetchError } from 'ofetch'
|
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
const client = createTRPCProxyClient<AppRouter>({
|
/**
|
||||||
|
* createTRPCNuxtClient adds a `useQuery` composable
|
||||||
|
* built on top of `useAsyncData`.
|
||||||
|
*/
|
||||||
|
const client = createTRPCNuxtClient<AppRouter>({
|
||||||
links: [
|
links: [
|
||||||
httpBatchLink({
|
httpBatchLink({
|
||||||
url: '/api/trpc',
|
url: '/api/trpc',
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace regular `fetch` with a `$fetch` from nuxt
|
|
||||||
*
|
|
||||||
* During server-side rendering, calling $fetch to fetch your internal API routes
|
|
||||||
* will directly call the relevant function (emulating the request),
|
|
||||||
* saving an additional API call.
|
|
||||||
*
|
|
||||||
* @see https://nuxt.com/docs/api/utils/dollarfetch
|
|
||||||
*/
|
|
||||||
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),
|
|
||||||
})),
|
|
||||||
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -150,16 +136,18 @@ export default defineNuxtPlugin(() => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3. Make API requests
|
## 3. Make an API request
|
||||||
|
|
||||||
```vue [pages/index.vue]
|
```vue [pages/index.vue]
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { $client } = useNuxtApp()
|
const { $client } = useNuxtApp()
|
||||||
|
|
||||||
const data = await $client.hello.query({ text: 'client' })
|
const hello = await $client.hello.useQuery({ text: 'client' })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<p>{{data?.greeting }}</p>
|
<div>
|
||||||
|
<p>{{ hello.data?.greeting }}</p>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
53
docs/content/1.get-started/3.client.md
Normal file
53
docs/content/1.get-started/3.client.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: Client
|
||||||
|
description: tRPC-Nuxt provides first class integration with tRPC.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nuxt client
|
||||||
|
|
||||||
|
The magic of tRPC is making strongly typed API calls without relying on code generation. With full-stack TypeScript projects, you can directly import types from the server into the client! This is a vital part of how tRPC works.
|
||||||
|
|
||||||
|
## Initialize a tRPC client
|
||||||
|
|
||||||
|
Create a typesafe client via a Nuxt [plugin](https://nuxt.com/docs/guide/directory-structure/plugins) with the `createTRPCNuxtClient` method from `trpc-nuxt/client`, and add a `links` array with a [terminating link](https://trpc.io/docs/links#the-terminating-link). If you want to learn more about tRPC links, check out the docs [here](https://trpc.io/docs/links):
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
`createTRPCNuxtClient` extends [createTRPCProxyClient](https://trpc.io/docs/vanilla#initialize-a-trpc-client) and adds a `useQuery` method built on top of [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data).
|
||||||
|
::
|
||||||
|
|
||||||
|
```ts [plugins/client.ts]
|
||||||
|
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
|
||||||
|
import type { AppRouter } from '~/server/trpc/routers'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const client = createTRPCNuxtClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: '/api/trpc',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
provide: {
|
||||||
|
client,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
As you can see, we passed `AppRouter` as a type argument of `createTRPCNuxtClient`. This returns a strongly typed `client` instance, a proxy that mirrors the structure of your `AppRouter` on the client:
|
||||||
|
|
||||||
|
```vue [pages/index.vue]
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $client } = useNuxtApp()
|
||||||
|
|
||||||
|
const getUser = await $client.getUser.useQuery('id_bilbo');
|
||||||
|
// => { data: { id: 'id_bilbo', name: 'Bilbo' }, pending: false, error: false };
|
||||||
|
|
||||||
|
const bilbo = await $client.getUser.query('id_bilbo');
|
||||||
|
// => { id: 'id_bilbo', name: 'Bilbo' };
|
||||||
|
const frodo = await $client.createUser.mutate({ name: 'Frodo' });
|
||||||
|
// => { id: 'id_frodo', name: 'Frodo' };
|
||||||
|
</script>
|
||||||
|
```
|
||||||
58
docs/content/1.get-started/4.links/1.httpLink.md
Normal file
58
docs/content/1.get-started/4.links/1.httpLink.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: HTTP Link
|
||||||
|
description: httpLink is a terminating link that sends a tRPC operation to a tRPC procedure over HTTP.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HTTP Link
|
||||||
|
|
||||||
|
`httpLink` is a [terminating link](https://trpc.io/docs/links#the-terminating-link) that sends a tRPC operation to a tRPC procedure over HTTP.
|
||||||
|
|
||||||
|
`httpLink` supports both POST and GET requests.
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
`httpLink` imported from `trpc-nuxt/client` is a convenience wrapper around the original `httpLink` that replaces regular `fetch` with a [`$fetch`](https://nuxt.com/docs/api/utils/dollarfetch) from Nuxt. It also sets the default headers using [`useRequestHeaders`](https://nuxt.com/docs/api/composables/use-request-headers#userequestheaders).
|
||||||
|
::
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You can import and add the `httpLink` to the `links` array as such:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createTRPCNuxtClient, httpLink } from 'trpc-nuxt/client'
|
||||||
|
import type { AppRouter } from '~/server/trpc/routers'
|
||||||
|
|
||||||
|
const client = createTRPCNuxtClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpLink({
|
||||||
|
url: '/api/trpc',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## `httpLink` Options
|
||||||
|
|
||||||
|
The `httpLink` function takes an options object that has the `HTTPLinkOptions` shape.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface HTTPLinkOptions {
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Select headers to pass to `useRequestHeaders`.
|
||||||
|
*/
|
||||||
|
pickHeaders?: string[];
|
||||||
|
/**
|
||||||
|
* Add ponyfill for fetch.
|
||||||
|
*/
|
||||||
|
fetch?: typeof fetch;
|
||||||
|
/**
|
||||||
|
* Add ponyfill for AbortController
|
||||||
|
*/
|
||||||
|
AbortController?: typeof AbortController | null;
|
||||||
|
/**
|
||||||
|
* Headers to be set on outgoing requests or a callback that of said headers
|
||||||
|
* @link http://trpc.io/docs/v10/header
|
||||||
|
*/
|
||||||
|
headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);
|
||||||
|
}
|
||||||
|
```
|
||||||
88
docs/content/1.get-started/4.links/2.httpBatchLink.md
Normal file
88
docs/content/1.get-started/4.links/2.httpBatchLink.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: HTTP Batch Link
|
||||||
|
description: httpBatchLink is a terminating link that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HTTP Batch Link
|
||||||
|
|
||||||
|
`httpBatchLink` is a [terminating link](https://trpc.io/docs/links#the-terminating-link) that batches an array of individual tRPC operations into a single HTTP request that's sent to a single tRPC procedure.
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
`httpBatchLink` imported from `trpc-nuxt/client` is a convenience wrapper around the original `httpBatchLink` that replaces regular `fetch` with a [`$fetch`](https://nuxt.com/docs/api/utils/dollarfetch) from Nuxt. It also sets the default headers using [`useRequestHeaders`](https://nuxt.com/docs/api/composables/use-request-headers#userequestheaders).
|
||||||
|
::
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You can import and add the `httpBatchLink` to the `links` array as such:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client'
|
||||||
|
import type { AppRouter } from '~/server/trpc/routers'
|
||||||
|
|
||||||
|
const client = createTRPCNuxtClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: '/api/trpc',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
After that, you can make use of batching by setting all your procedures in a `Promise.all`. The code below will produce exactly one HTTP request and on the server exactly `one` database query:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const somePosts = await Promise.all([
|
||||||
|
$client.post.byId.query(1);
|
||||||
|
$client.post.byId.query(2);
|
||||||
|
$client.post.byId.query(3);
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
## `httpBatchLink` Options
|
||||||
|
|
||||||
|
The `httpBatchLink` function takes an options object that has the `HTTPBatchLinkOptions` shape.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface HttpBatchLinkOptions extends HTTPLinkOptions {
|
||||||
|
maxURLLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HTTPLinkOptions {
|
||||||
|
url: string;
|
||||||
|
/**
|
||||||
|
* Select headers to pass to `useRequestHeaders`.
|
||||||
|
*/
|
||||||
|
pickHeaders?: string[];
|
||||||
|
/**
|
||||||
|
* Add ponyfill for fetch.
|
||||||
|
*/
|
||||||
|
fetch?: typeof fetch;
|
||||||
|
/**
|
||||||
|
* Add ponyfill for AbortController
|
||||||
|
*/
|
||||||
|
AbortController?: typeof AbortController | null;
|
||||||
|
/**
|
||||||
|
* Headers to be set on outgoing requests or a callback that of said headers
|
||||||
|
* @link http://trpc.io/docs/v10/header
|
||||||
|
*/
|
||||||
|
headers?: HTTPHeaders | (() => HTTPHeaders | Promise<HTTPHeaders>);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting a maximum URL length
|
||||||
|
|
||||||
|
When sending batch requests, sometimes the URL can become too large causing HTTP errors like [413 Payload Too Large](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413), [414 URI Too Long](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414), and [404 Not Found](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404). The `maxURLLength` option will limit the number of requests that can be sent together in a batch.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createTRPCNuxtClient, httpBatchLink } from 'trpc-nuxt/client';
|
||||||
|
import type { AppRouter } from '~/server/trpc/routers'
|
||||||
|
|
||||||
|
const client = createTRPCNuxtClient<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: '/api/trpc',
|
||||||
|
maxURLLength: 2083, // a suitable size
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
@@ -6,6 +6,10 @@ title: Composables
|
|||||||
|
|
||||||
It is often useful to wrap functionality of your `@trpc/client` api within other functions. For this purpose, it's necessary to be able to infer input types and output types generated by your `@trpc/server` router.
|
It is often useful to wrap functionality of your `@trpc/client` api within other functions. For this purpose, it's necessary to be able to infer input types and output types generated by your `@trpc/server` router.
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
[createTRPCNuxtClient](/get-started/client/create) adds a `useQuery` method built on top of [useAsyncData](https://nuxt.com/docs/api/composables/use-async-data/).
|
||||||
|
::
|
||||||
|
|
||||||
## Inference Helpers
|
## Inference Helpers
|
||||||
|
|
||||||
`@trpc/server` exports the following helper types to assist with inferring these types from the `AppRouter` exported by your `@trpc/server` router:
|
`@trpc/server` exports the following helper types to assist with inferring these types from the `AppRouter` exported by your `@trpc/server` router:
|
||||||
@@ -6,6 +6,10 @@ title: Headers
|
|||||||
|
|
||||||
We can use the built-in [useRequestHeaders](https://v3.nuxtjs.org/api/composables/use-request-headers/) to set outgoing request headers:
|
We can use the built-in [useRequestHeaders](https://v3.nuxtjs.org/api/composables/use-request-headers/) to set outgoing request headers:
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
[createTRPCNuxtClient](/get-started/client/create) has this feature by default.
|
||||||
|
::
|
||||||
|
|
||||||
```ts [plugins/client.ts]
|
```ts [plugins/client.ts]
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
const headers = useRequestHeaders()
|
const headers = useRequestHeaders()
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"nuxt": "^3.0.0"
|
"nuxt": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxt-themes/docus": "^0.3.1",
|
"@nuxt-themes/docus": "^1.1.10",
|
||||||
"@nuxtlabs/github-module": "^1.5.3"
|
"@nuxtlabs/github-module": "^1.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
package.json
31
package.json
@@ -2,6 +2,7 @@
|
|||||||
"name": "trpc-nuxt",
|
"name": "trpc-nuxt",
|
||||||
"description": "End-to-end typesafe APIs in Nuxt applications.",
|
"description": "End-to-end typesafe APIs in Nuxt applications.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@7.18.2",
|
||||||
"version": "0.4.3",
|
"version": "0.4.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
@@ -9,13 +10,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\"",
|
||||||
@@ -31,15 +38,15 @@
|
|||||||
"@trpc/server": "^10.0.0"
|
"@trpc/server": "^10.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"h3": "^1.0.1",
|
"h3": "^1.0.2",
|
||||||
"nanoid": "^4.0.0",
|
"ofetch": "^1.0.0",
|
||||||
"ohash": "^1.0.0",
|
"ohash": "^1.0.0",
|
||||||
"ufo": "^1.0.0"
|
"ufo": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nuxtjs/eslint-config-typescript": "^11.0.0",
|
"@nuxt/eslint-config": "^0.1.1",
|
||||||
"@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",
|
||||||
@@ -48,7 +55,7 @@
|
|||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"@nuxtjs/eslint-config-typescript"
|
"@nuxt/eslint-config"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": [
|
||||||
@@ -64,5 +71,13 @@
|
|||||||
"*.md",
|
"*.md",
|
||||||
"dist",
|
"dist",
|
||||||
".output"
|
".output"
|
||||||
]
|
],
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"nuxt": "3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.13.0 || ^18.12.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
|
build: {
|
||||||
|
transpile: ['trpc-nuxt/client']
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
<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 addTodo = async () => {
|
const addTodo = async () => {
|
||||||
@@ -21,10 +17,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>
|
||||||
@@ -37,8 +30,14 @@ const { data: todos, pending, error, refresh } = await useAsyncData<RouterOutput
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="t in todos?.slice(0, 10)" :key="t.id">
|
<li
|
||||||
<NuxtLink :class="{ completed: t.completed }" :to="`/todo/${t.id}`">
|
v-for="t in todos?.slice(0, 10)"
|
||||||
|
:key="t.id"
|
||||||
|
>
|
||||||
|
<NuxtLink
|
||||||
|
:class="{ completed: t.completed }"
|
||||||
|
:to="`/todo/${t.id}`"
|
||||||
|
>
|
||||||
Title: {{ t.title }}
|
Title: {{ t.title }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { createTRPCProxyClient, httpBatchLink, loggerLink } from '@trpc/client'
|
import { loggerLink } from '@trpc/client'
|
||||||
import superjson from 'superjson'
|
import superjson from 'superjson'
|
||||||
|
import { createTRPCNuxtClient, httpBatchLink } 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 client = createTRPCNuxtClient<AppRouter>({
|
||||||
const client = createTRPCProxyClient<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
|
||||||
@@ -13,12 +13,7 @@ export default defineNuxtPlugin(() => {
|
|||||||
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',
|
|
||||||
headers () {
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
2335
pnpm-lock.yaml
generated
2335
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
71
src/client/index.ts
Normal file
71
src/client/index.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { type CreateTRPCClientOptions, type inferRouterProxyClient, createTRPCProxyClient } from '@trpc/client'
|
||||||
|
import { type AnyRouter } from '@trpc/server'
|
||||||
|
import { createFlatProxy, createRecursiveProxy } from '@trpc/server/shared'
|
||||||
|
import { hash } from 'ohash'
|
||||||
|
import { type DecoratedProcedureRecord } from './types'
|
||||||
|
// @ts-expect-error: Nuxt auto-imports
|
||||||
|
import { getCurrentInstance, onScopeDispose, useAsyncData } 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
|
||||||
|
|
||||||
|
if (lastArg === 'useQuery') {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryKey = getQueryKey(path, input)
|
||||||
|
return useAsyncData(queryKey, () => (client as any)[path].query(input, {
|
||||||
|
signal: controller?.signal,
|
||||||
|
...trpc
|
||||||
|
}), asyncDataOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
httpBatchLink,
|
||||||
|
httpLink
|
||||||
|
} from './links'
|
||||||
78
src/client/links.ts
Normal file
78
src/client/links.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { httpLink as _httpLink, httpBatchLink as _httpBatchLink } from '@trpc/client'
|
||||||
|
import { type AnyRouter } from '@trpc/server'
|
||||||
|
import { FetchError } from 'ofetch'
|
||||||
|
// @ts-expect-error: Nuxt auto-imports
|
||||||
|
import { useRequestHeaders } from '#imports'
|
||||||
|
import { type HTTPLinkOptions as _HTTPLinkOptions } from '@trpc/client/dist/links/internals/httpUtils'
|
||||||
|
|
||||||
|
function customFetch(input: RequestInfo | URL, init?: RequestInit) {
|
||||||
|
return globalThis.$fetch.raw(input.toString(), init)
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof FetchError && e.response) { return e.response }
|
||||||
|
throw e
|
||||||
|
})
|
||||||
|
.then(response => ({
|
||||||
|
...response,
|
||||||
|
json: () => Promise.resolve(response._data)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HTTPLinkOptions extends _HTTPLinkOptions {
|
||||||
|
/**
|
||||||
|
* Select headers to pass to `useRequestHeaders`.
|
||||||
|
*/
|
||||||
|
pickHeaders?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a convenience wrapper around the original httpLink
|
||||||
|
* that replaces regular `fetch` with a `$fetch` from Nuxt. It
|
||||||
|
* also sets the default headers based on `useRequestHeaders` values.
|
||||||
|
*
|
||||||
|
* During server-side rendering, calling $fetch to fetch your internal API routes
|
||||||
|
* will directly call the relevant function (emulating the request),
|
||||||
|
* saving an additional API call.
|
||||||
|
*
|
||||||
|
* @see https://nuxt.com/docs/api/utils/dollarfetch
|
||||||
|
*/
|
||||||
|
export function httpLink<TRouter extends AnyRouter>(opts?: HTTPLinkOptions) {
|
||||||
|
const headers = useRequestHeaders(opts?.pickHeaders)
|
||||||
|
|
||||||
|
return _httpLink<TRouter>({
|
||||||
|
url: '/api/trpc',
|
||||||
|
headers () {
|
||||||
|
return headers
|
||||||
|
},
|
||||||
|
fetch: customFetch,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HttpBatchLinkOptions extends HTTPLinkOptions {
|
||||||
|
maxURLLength?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a convenience wrapper around the original httpBatchLink
|
||||||
|
* that replaces regular `fetch` with a `$fetch` from Nuxt. It
|
||||||
|
* also sets the default headers based on `useRequestHeaders` values.
|
||||||
|
*
|
||||||
|
* During server-side rendering, calling $fetch to fetch your internal API routes
|
||||||
|
* will directly call the relevant function (emulating the request),
|
||||||
|
* saving an additional API call.
|
||||||
|
*
|
||||||
|
* @see https://nuxt.com/docs/api/utils/dollarfetch
|
||||||
|
*/
|
||||||
|
export function httpBatchLink<TRouter extends AnyRouter>(opts?: HttpBatchLinkOptions) {
|
||||||
|
const headers = useRequestHeaders(opts?.pickHeaders)
|
||||||
|
|
||||||
|
return _httpBatchLink<TRouter>({
|
||||||
|
url: '/api/trpc',
|
||||||
|
headers () {
|
||||||
|
return headers
|
||||||
|
},
|
||||||
|
fetch: customFetch,
|
||||||
|
...opts,
|
||||||
|
})
|
||||||
|
}
|
||||||
80
src/client/types.ts
Normal file
80
src/client/types.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
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 type {
|
||||||
|
AsyncData,
|
||||||
|
AsyncDataOptions,
|
||||||
|
KeyOfRes,
|
||||||
|
PickFrom,
|
||||||
|
_Transform
|
||||||
|
} from 'nuxt/dist/app/composables/asyncData'
|
||||||
|
|
||||||
|
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 ? {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user