orpc-nuxt 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Inspired by [trpc-nuxt](https://github.com/wobsoriano/trpc-nuxt).
16
16
  npm install orpc-nuxt @orpc/client@2.0.0-beta.35 @orpc/server@2.0.0-beta.35 @orpc/tanstack-query@2.0.0-beta.35 @tanstack/vue-query
17
17
  ```
18
18
 
19
- Use the oRPC v2 beta versions shown above.
19
+ Install oRPC by version: v2 is still in beta, and this package needs `2.0.0-beta.35` or newer.
20
20
 
21
21
  ## Setup
22
22
 
@@ -68,8 +68,6 @@ const query = await orpc.blog.posts.get.useQuery({ id: 1 })
68
68
  - `query.refetch()` fetches the current query again.
69
69
  - `query.invalidate()` marks its cached result as stale and refetches it if it is active.
70
70
 
71
- You can also access the client as `useNuxtApp().$orpc`; both accessors infer types from your plugin.
72
-
73
71
  ### Reactive input and options
74
72
 
75
73
  Pass a ref, reactive object, or getter when the input can change.
@@ -165,7 +163,29 @@ Invalidation marks matching queries as stale and refetches active ones.
165
163
  Inactive queries can refresh when used again.
166
164
  Create the decorated client inside your app plugin so callbacks use that app's QueryClient.
167
165
 
168
- ## Updating cached data
166
+ ## Direct calls and oRPC utilities
167
+
168
+ Use `.call()` when you just need a procedure's response, without query state or caching:
169
+
170
+ ```ts
171
+ const post = await orpc.blog.posts.get.call({ id: 1 })
172
+ ```
173
+
174
+ The client also exposes oRPC's utilities:
175
+
176
+ - `.key()` builds a cache key prefix for a procedure or router branch.
177
+ - `.queryKey()` builds a query key for a specific input.
178
+ - `.queryOptions()` builds options for Vue Query's `useQuery`.
179
+ - `.mutationOptions()` builds options for Vue Query's `useMutation`.
180
+ - `.infiniteOptions()` builds options for Vue Query's `useInfiniteQuery` for pagination.
181
+
182
+ Subscription composables are not available yet.
183
+ Procedures that return streams keep the oRPC utilities but do not get this package's `useQuery` or `useMutation` methods.
184
+ The module does not automatically transfer streamed query results from server to browser.
185
+
186
+ ## Query data
187
+
188
+ ### Updating cached data
169
189
 
170
190
  Assigning a new response to `data.value` updates the shared cache for the query's current input.
171
191
  Other components reading the same query see the update too.
@@ -173,19 +193,19 @@ Nested properties are readonly unless you enable `clone: true`.
173
193
 
174
194
  ```ts
175
195
  const { data } = await orpc.blog.posts.get.useQuery({ id: 1 })
176
- const savePost = orpc.blog.posts.update.useMutation()
177
196
 
178
- const response = await savePost.mutateAsync({
197
+ const response = await orpc.blog.posts.update.call({
179
198
  id: 1,
180
199
  title: "Updated title",
181
200
  })
201
+
182
202
  data.value = response
183
203
  ```
184
204
 
185
205
  Treat the original `response` as readonly after assigning it to `data.value`.
186
206
  The assignment passes it to the cache without a defensive copy, so changing `response.title` could modify cached data directly.
187
207
 
188
- ## Editing drafts
208
+ ### Mutable data
189
209
 
190
210
  With `clone: true`, `data.value` contains a reactive local copy that you can edit, for example in a form.
191
211
  There are two ways to change it:
@@ -197,14 +217,12 @@ There are two ways to change it:
197
217
  const id = 1
198
218
  const { data } = await orpc.blog.posts.get.useQuery({ id }, { clone: true })
199
219
 
200
- const savePost = orpc.blog.posts.update.useMutation()
201
-
202
220
  if (data.value) {
203
221
  // Only this query's local copy changes.
204
222
  data.value.title = "Local draft"
205
223
 
206
224
  // Save the draft, then share the server's response with other components.
207
- data.value = await savePost.mutateAsync({
225
+ data.value = await orpc.blog.posts.update.call({
208
226
  id,
209
227
  title: data.value.title,
210
228
  })
@@ -219,21 +237,6 @@ Keep a separate form draft if it must survive these updates.
219
237
 
220
238
  You cannot combine `clone: true` with `select`; selected results are readonly.
221
239
 
222
- ## Direct calls and oRPC utilities
223
-
224
- Use `.call()` when you just need a procedure's response, without query state or caching:
225
-
226
- ```ts
227
- const post = await orpc.blog.posts.get.call({ id: 1 })
228
- ```
229
-
230
- The client also exposes oRPC's `.key()`, `.queryKey()`, `.queryOptions()`, `.mutationOptions()`, and `.infiniteOptions()` utilities.
231
- You can pass their options to Vue Query composables, for example `.infiniteOptions()` to `useInfiniteQuery` for pagination.
232
-
233
- Subscription composables are not available yet.
234
- Procedures that return streams keep the oRPC utilities but do not get this package's `useQuery` or `useMutation` methods.
235
- The module does not automatically transfer streamed query results from server to browser.
236
-
237
240
  ## Advanced
238
241
 
239
242
  ### Separate API service
@@ -291,6 +294,9 @@ Only headers listed in `forwardHeaders` are forwarded from the incoming SSR requ
291
294
  Use `createORPCNuxtClient` when you need a custom transport or want SSR to call the router directly.
292
295
  Instead of the shared HTTP plugin above, add a browser plugin and a server plugin.
293
296
 
297
+ Keep `orpc-nuxt` in `modules`: manual setup replaces that plugin, not the module that installs the QueryClient and the composables.
298
+ Both plugins provide the client under the `orpc` key: `useOrpc()` reads it as `useNuxtApp().$orpc` and infers the router type from that injection.
299
+
294
300
  The browser plugin sends requests to `/rpc` over HTTP:
295
301
 
296
302
  ```ts
@@ -331,6 +337,9 @@ export default defineNuxtPlugin(() => {
331
337
  })
332
338
  ```
333
339
 
340
+ Let both plugins infer the client type instead of annotating it.
341
+ Nuxt combines what they provide, so a widened type in either one leaves `useOrpc()` without procedure types.
342
+
334
343
  ### SSR and cache configuration
335
344
 
336
345
  The module gives each server request its own QueryClient, which manages the query cache.
@@ -385,6 +394,19 @@ export default defineNuxtPlugin({
385
394
 
386
395
  Nuxt registers the handlers declared in `hooks` before running plugins, so this handler is ready when the module creates the QueryClient.
387
396
 
397
+ ### Query client
398
+
399
+ `useOrpcQueryClient()` returns the QueryClient installed for the app, whether by the module or by your own plugin.
400
+ Unlike Vue Query's `useQueryClient()`, it also works where Vue injection is unavailable, such as in an event handler or between tests:
401
+
402
+ ```ts
403
+ const queryClient = useOrpcQueryClient()
404
+ queryClient.clear()
405
+ ```
406
+
407
+ It needs the Nuxt context, which the browser keeps available once the app has started.
408
+ During server rendering, call it inside `nuxtApp.runWithContext()`.
409
+
388
410
  ### Existing Vue Query setup
389
411
 
390
412
  If your app already installs Vue Query and transfers its cache between server and browser, disable the module's QueryClient setup:
@@ -408,9 +430,71 @@ If you have multiple oRPC clients with the same procedure paths, give each a dif
408
430
  const orpc = createORPCNuxtClient(client, { prefix: "blog" })
409
431
  ```
410
432
 
433
+ ### Component tests
434
+
435
+ Component tests need the Nuxt environment of `@nuxt/test-utils`:
436
+
437
+ ```ts
438
+ // vitest.config.ts
439
+ import { defineVitestConfig } from "@nuxt/test-utils/config"
440
+
441
+ export default defineVitestConfig({})
442
+ ```
443
+
444
+ That configuration runs every test under `test/nuxt/` or `tests/nuxt/`, and every test named `*.nuxt.test.ts` or `*.nuxt.spec.ts`, against your application.
445
+ Replace `useOrpc()` with a client that returns fixed data, then mount the component with `mountSuspended()` so its awaited queries resolve before the assertions.
446
+ The replacement can be any nested object of async functions; only the procedures the component calls have to exist.
447
+
448
+ ```ts
449
+ // test/nuxt/post-list.spec.ts
450
+ import { mockNuxtImport, mountSuspended } from "@nuxt/test-utils/runtime"
451
+ import { QueryClient } from "@tanstack/vue-query"
452
+ import { createORPCNuxtClient } from "orpc-nuxt/client"
453
+ import { afterEach, expect, test } from "vitest"
454
+
455
+ import PostList from "~/components/post-list.vue"
456
+
457
+ const queryClient = new QueryClient({
458
+ // Report a failing procedure instead of retrying it until the test times out.
459
+ defaultOptions: { queries: { retry: false } },
460
+ })
461
+
462
+ const orpc = createORPCNuxtClient(
463
+ {
464
+ blog: {
465
+ posts: {
466
+ list: async () => [{ id: 1, title: "First post" }],
467
+ },
468
+ },
469
+ },
470
+ { queryClient },
471
+ )
472
+
473
+ // Vitest runs this factory before the file body, so it must return the composable without calling it.
474
+ mockNuxtImport("useOrpc", () => () => orpc)
475
+
476
+ afterEach(() => {
477
+ // Start every test from an empty cache, so an earlier response cannot satisfy a later query.
478
+ queryClient.clear()
479
+ })
480
+
481
+ test("renders the posts", async () => {
482
+ const component = await mountSuspended(PostList)
483
+ expect(component.text()).toContain("First post")
484
+ })
485
+ ```
486
+
487
+ Passing an explicit `queryClient` gives the tests their own cache:
488
+
489
+ - The application's cache never carries data from one test into the next.
490
+ - Test defaults such as `retry: false` stay in the test file instead of the module options.
491
+
492
+ Omit it to test against the cache the module installs.
493
+ Read that one with `useOrpcQueryClient()`, and set its defaults through the `orpc.queryClient` module options.
494
+
411
495
  ### Outside Vue components
412
496
 
413
- You can call `.useQuery()` and `.useMutation()` outside a component, for example in tests.
497
+ You can call `.useQuery()` and `.useMutation()` outside a component, for example in a script or in a test that never mounts one.
414
498
  Create them inside `scope.run()` so Vue can track their reactive subscriptions, then call `scope.stop()` when you are done.
415
499
  When Vue injection is unavailable, pass a QueryClient explicitly:
416
500
 
@@ -439,9 +523,12 @@ try {
439
523
 
440
524
  Install dependencies with `bun install`, then run `bun run build`, `bun run types`, and `bun run test`.
441
525
 
526
+ Run `bun run test:component` to check the documented component-test recipe in the fixture application; it uses the built package, so build first.
527
+
442
528
  To try the package in a Nuxt app, build it and run `bunx nuxt dev tests/fixtures/nuxt`.
443
529
  The example app uses the built package, so rebuild after changing its source.
444
530
 
445
- Run `bunx playwright install chromium`, then `bun run test:nuxt` to check the packed npm archive with Nuxt 3.17.5 and Nuxt 4.5.2.
446
- Each version is checked with both module-managed and app-managed QueryClients, including types, SSR, and hydration in development and production.
447
- To check one version, use `bun run test:nuxt 3.17.5`.
531
+ Run `bunx playwright install chromium-headless-shell`, then `bun run test:nuxt` to check the packed npm archive with the Nuxt version installed in the workspace.
532
+ It is checked with both module-managed and app-managed QueryClients, including types, SSR, and hydration in development and production.
533
+
534
+ To check other Nuxt versions by hand, pass them explicitly: `bun run test:nuxt 3.14.1592 4.0.1`.
package/dist/module.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "name": "orpc-nuxt",
3
3
  "configKey": "orpc",
4
4
  "compatibility": {
5
- "nuxt": "^3.17.5 || ^4.0.0"
5
+ "nuxt": "^3.14.1592 || ^4.0.1"
6
6
  },
7
- "version": "0.1.0",
7
+ "version": "0.2.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -5,7 +5,7 @@ const module$1 = defineNuxtModule({
5
5
  meta: {
6
6
  name: "orpc-nuxt",
7
7
  configKey: "orpc",
8
- compatibility: { nuxt: "^3.17.5 || ^4.0.0" }
8
+ compatibility: { nuxt: "^3.14.1592 || ^4.0.1" }
9
9
  },
10
10
  defaults: { queryClient: true },
11
11
  setup(options, nuxt) {
@@ -14,10 +14,11 @@ const module$1 = defineNuxtModule({
14
14
  const optimizeDeps = nuxt.options.vite.optimizeDeps ??= {};
15
15
  optimizeDeps.exclude ??= [];
16
16
  optimizeDeps.exclude.push("orpc-nuxt/client", "@tanstack/vue-query");
17
- addImports({
18
- name: "useOrpc",
19
- from: resolver.resolve("./runtime/composables")
20
- });
17
+ const composables = resolver.resolve("./runtime/composables");
18
+ addImports([
19
+ { name: "useOrpc", from: composables },
20
+ { name: "useOrpcQueryClient", from: composables }
21
+ ]);
21
22
  if (options.queryClient) {
22
23
  const config = options.queryClient === true ? {} : options.queryClient;
23
24
  addPluginTemplate({
@@ -1,29 +1,26 @@
1
1
  import { createTanstackQueryUtils } from "@orpc/tanstack-query";
2
- import {
3
- useQueryClient,
4
- VUE_QUERY_CLIENT
5
- } from "@tanstack/vue-query";
6
- import { hasInjectionContext, inject } from "vue";
2
+ import { useQueryClient } from "@tanstack/vue-query";
7
3
  import { useORPCMutation } from "../vue-query/mutation.js";
8
4
  import { useORPCQuery } from "../vue-query/query.js";
5
+ import { resolveQueryClient } from "../vue-query/query-client.js";
9
6
  import { decorateClient } from "./decorate.js";
10
7
  export function createORPCNuxtClient(client, options = {}) {
11
8
  const utils = createTanstackQueryUtils(client, { prefix: options.prefix });
12
- let queryClient = options.queryClient ?? (hasInjectionContext() ? inject(VUE_QUERY_CLIENT, void 0) : void 0);
13
- function resolveQueryClient() {
9
+ let queryClient = options.queryClient ?? resolveQueryClient();
10
+ function getQueryClient() {
14
11
  return queryClient ??= useQueryClient();
15
12
  }
16
13
  function createMethods(target) {
17
14
  return {
18
15
  useQuery(input, queryOptions) {
19
- return useORPCQuery(target, input, queryOptions, resolveQueryClient());
16
+ return useORPCQuery(target, input, queryOptions, getQueryClient());
20
17
  },
21
18
  useMutation(mutationOptions) {
22
- return useORPCMutation(target, mutationOptions, resolveQueryClient());
19
+ return useORPCMutation(target, mutationOptions, getQueryClient());
23
20
  },
24
21
  invalidate() {
25
22
  const utils2 = target;
26
- return resolveQueryClient().invalidateQueries({ queryKey: utils2.key() });
23
+ return getQueryClient().invalidateQueries({ queryKey: utils2.key() });
27
24
  }
28
25
  };
29
26
  }
@@ -1 +1 @@
1
- export { useOrpc } from "./nuxt/composables.js";
1
+ export { useOrpc, useOrpcQueryClient } from "./nuxt/composables.js";
@@ -1 +1 @@
1
- export { useOrpc } from "./nuxt/composables.js";
1
+ export { useOrpc, useOrpcQueryClient } from "./nuxt/composables.js";
@@ -1,3 +1,4 @@
1
+ import type { QueryClient } from "@tanstack/vue-query";
1
2
  import { type NuxtApp } from "nuxt/app";
2
3
  type InjectedORPCClient = NuxtApp extends {
3
4
  $orpc: infer TClient;
@@ -8,4 +9,13 @@ type InjectedORPCClient = NuxtApp extends {
8
9
  * The router type is inferred from that plugin's return value.
9
10
  */
10
11
  export declare function useOrpc(): InjectedORPCClient;
12
+ /**
13
+ * Read the QueryClient installed for this Nuxt app, by the module or by your own plugin.
14
+ * Unlike Vue Query's own accessor, this ignores component-level providers and works outside
15
+ * a component, for example in an event handler or when clearing the cache between tests.
16
+ * The browser keeps the Nuxt context available after startup; during server rendering,
17
+ * call this inside nuxtApp.runWithContext().
18
+ * Throws when no QueryClient is installed, as with queryClient: false and no Vue Query plugin.
19
+ */
20
+ export declare function useOrpcQueryClient(): QueryClient;
11
21
  export {};
@@ -1,4 +1,5 @@
1
1
  import { useNuxtApp } from "nuxt/app";
2
+ import { resolveQueryClient } from "../vue-query/query-client.js";
2
3
  export function useOrpc() {
3
4
  const client = useNuxtApp().$orpc;
4
5
  if (!client) {
@@ -6,3 +7,12 @@ export function useOrpc() {
6
7
  }
7
8
  return client;
8
9
  }
10
+ export function useOrpcQueryClient() {
11
+ const queryClient = resolveQueryClient(useNuxtApp().vueApp);
12
+ if (!queryClient) {
13
+ throw new Error(
14
+ "Install Vue Query before calling useOrpcQueryClient(). Enable the module's queryClient option, or install Vue Query from a Nuxt plugin."
15
+ );
16
+ }
17
+ return queryClient;
18
+ }
@@ -1,17 +1,16 @@
1
1
  import { createORPCClient } from "@orpc/client";
2
2
  import { RPCLink } from "@orpc/client/fetch";
3
- import { VUE_QUERY_CLIENT } from "@tanstack/vue-query";
4
3
  import {
5
4
  defineNuxtPlugin as createNuxtPlugin,
6
5
  useRequestHeaders,
7
6
  useRequestURL
8
7
  } from "nuxt/app";
9
- import { inject } from "vue";
10
8
  import { createORPCNuxtClient } from "../client/create.js";
9
+ import { resolveQueryClient } from "../vue-query/query-client.js";
11
10
  export function defineNuxtPlugin(setup) {
12
11
  return createNuxtPlugin((nuxtApp) => {
13
12
  const options = setup(nuxtApp);
14
- const queryClient = options.queryClient ?? inject(VUE_QUERY_CLIENT, void 0);
13
+ const queryClient = options.queryClient ?? resolveQueryClient(nuxtApp.vueApp);
15
14
  if (!queryClient) {
16
15
  throw new Error(
17
16
  "orpc-nuxt: install Vue Query before the oRPC plugin. Enable the module's QueryClient, use enforce: 'pre' in your Vue Query plugin, or pass queryClient explicitly."
@@ -0,0 +1,12 @@
1
+ import { type QueryClient } from "@tanstack/vue-query";
2
+ import { type App } from "vue";
3
+ /**
4
+ * Find the QueryClient installed by Vue Query, with or without an active injection context.
5
+ * An application is read directly, so a component providing its own cache cannot shadow the one
6
+ * a plugin captured for the whole application.
7
+ * Without an application, the active injection context is the only source.
8
+ *
9
+ * @param app - The application to read, whatever context the call happens in.
10
+ * @returns The installed QueryClient, or undefined when Vue Query is unavailable.
11
+ */
12
+ export declare function resolveQueryClient(app?: App): QueryClient | undefined;
@@ -0,0 +1,9 @@
1
+ import { VUE_QUERY_CLIENT } from "@tanstack/vue-query";
2
+ import { hasInjectionContext, inject } from "vue";
3
+ export function resolveQueryClient(app) {
4
+ if (app) return app.runWithContext(injectQueryClient);
5
+ return hasInjectionContext() ? injectQueryClient() : void 0;
6
+ }
7
+ function injectQueryClient() {
8
+ return inject(VUE_QUERY_CLIENT, void 0);
9
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "orpc-nuxt",
3
- "version": "0.1.0",
4
- "description": "oRPC integration for Nuxt.",
3
+ "version": "0.2.0",
4
+ "description": "oRPC integration for Nuxt with TanStack Vue Query composables.",
5
5
  "homepage": "https://github.com/IlyaSemenov/orpc-nuxt#readme",
6
6
  "bugs": "https://github.com/IlyaSemenov/orpc-nuxt/issues",
7
7
  "license": "MIT",
@@ -46,36 +46,41 @@
46
46
  "prepare": "lefthook install",
47
47
  "prepublishOnly": "bun run build",
48
48
  "test": "bun test src tests/runtime",
49
+ "test:component": "vitest run --root tests/fixtures/nuxt",
49
50
  "test:nuxt": "bun tests/nuxt/run.ts",
50
51
  "types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json && nuxt typecheck tests/fixtures/nuxt"
51
52
  },
52
53
  "dependencies": {
53
- "@nuxt/kit": "^4.5.2",
54
- "devalue": "^5.9.2",
54
+ "@nuxt/kit": "^3.14.1592 || ^4.0.1",
55
+ "devalue": "^5.0.0",
55
56
  "es-toolkit": "^1.52.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@changesets/cli": "^2.31.1",
59
60
  "@nuxt/cli": "^3.37.0",
60
61
  "@nuxt/module-builder": "^1.0.3",
62
+ "@nuxt/test-utils": "^4.3.2",
61
63
  "@orpc/server": "2.0.0-beta.35",
62
64
  "@playwright/test": "1.58.2",
63
65
  "@tsconfig/bun": "^1.0.10",
64
66
  "@types/bun": "^1.3.14",
65
67
  "@vue/server-renderer": "^3.5.0",
68
+ "@vue/test-utils": "^2.5.0",
69
+ "happy-dom": "^20.14.3",
66
70
  "nuxt": "^4.5.2",
67
71
  "oxfmt": "^0.67.0",
68
72
  "oxlint": "^1.82.0",
69
73
  "publint": "^0.3.22",
70
74
  "typescript": "^5.9.3",
75
+ "vitest": "^5.0.0",
71
76
  "vue-tsc": "^3.3.11",
72
77
  "zod": "^4.3.6"
73
78
  },
74
79
  "peerDependencies": {
75
- "@orpc/client": "2.0.0-beta.35",
76
- "@orpc/tanstack-query": "2.0.0-beta.35",
80
+ "@orpc/client": "^2.0.0-beta.35",
81
+ "@orpc/tanstack-query": "^2.0.0-beta.35",
77
82
  "@tanstack/vue-query": "^5.102.8",
78
- "nuxt": "^3.17.5 || ^4.0.0",
83
+ "nuxt": "^3.14.1592 || ^4.0.1",
79
84
  "vue": "^3.5.0"
80
85
  },
81
86
  "packageManager": "bun@1.3.14"