orpc-nuxt 0.1.1 → 0.3.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.
@@ -296,6 +294,9 @@ Only headers listed in `forwardHeaders` are forwarded from the incoming SSR requ
296
294
  Use `createORPCNuxtClient` when you need a custom transport or want SSR to call the router directly.
297
295
  Instead of the shared HTTP plugin above, add a browser plugin and a server plugin.
298
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
+
299
300
  The browser plugin sends requests to `/rpc` over HTTP:
300
301
 
301
302
  ```ts
@@ -336,6 +337,9 @@ export default defineNuxtPlugin(() => {
336
337
  })
337
338
  ```
338
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
+
339
343
  ### SSR and cache configuration
340
344
 
341
345
  The module gives each server request its own QueryClient, which manages the query cache.
@@ -390,6 +394,19 @@ export default defineNuxtPlugin({
390
394
 
391
395
  Nuxt registers the handlers declared in `hooks` before running plugins, so this handler is ready when the module creates the QueryClient.
392
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
+
393
410
  ### Existing Vue Query setup
394
411
 
395
412
  If your app already installs Vue Query and transfers its cache between server and browser, disable the module's QueryClient setup:
@@ -413,9 +430,82 @@ If you have multiple oRPC clients with the same procedure paths, give each a dif
413
430
  const orpc = createORPCNuxtClient(client, { prefix: "blog" })
414
431
  ```
415
432
 
433
+ ### Component tests
434
+
435
+ Configure the Nuxt test environment and load a shared setup file:
436
+
437
+ ```ts
438
+ // vitest.config.ts
439
+ import { defineVitestConfig } from "@nuxt/test-utils/config"
440
+
441
+ export default defineVitestConfig({
442
+ test: {
443
+ setupFiles: ["test/nuxt/setup.ts"],
444
+ },
445
+ })
446
+ ```
447
+
448
+ 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.
449
+ Use `createTestORPCClient()` from `orpc-nuxt/testing` to replace `useOrpc()` with an isolated fake client.
450
+ `client` includes the regular composables and oRPC utilities.
451
+ Each procedure leaf in `procedures` has a `.handle()` method that registers a typed handler and returns a Vitest mock.
452
+
453
+ ```ts
454
+ // test/nuxt/setup.ts
455
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime"
456
+ import type { RouterClient } from "@orpc/server"
457
+ import { QueryClient } from "@tanstack/vue-query"
458
+ import { createTestORPCClient } from "orpc-nuxt/testing"
459
+ import { afterEach } from "vitest"
460
+
461
+ import type { router } from "~~/server/rpc/router"
462
+
463
+ const queryClient = new QueryClient({
464
+ // Report a failing procedure instead of retrying it until the test times out.
465
+ defaultOptions: { queries: { retry: false } },
466
+ })
467
+
468
+ export const { client, procedures, reset } = createTestORPCClient<RouterClient<typeof router>>({
469
+ queryClient,
470
+ })
471
+
472
+ // Return the composable itself; Vitest hoists this factory before the setup file runs.
473
+ mockNuxtImport("useOrpc", () => () => client)
474
+
475
+ afterEach(() => {
476
+ reset() // Remove registered handlers.
477
+ queryClient.clear() // Remove cached responses.
478
+ })
479
+ ```
480
+
481
+ `orpc-nuxt/testing` does not import `nuxt/app`, so the hoisted `mockNuxtImport()` factory can load it safely.
482
+ The explicit QueryClient keeps the test cache isolated and lets the setup clear it after each test.
483
+ To use the application's QueryClient instead, omit the option and configure its defaults through `orpc.queryClient`.
484
+ In that mode, import `useOrpcQueryClient()` only from a module that the hoisted factory cannot reach.
485
+
486
+ Register the required handlers before mounting; `mountSuspended()` waits for awaited queries before assertions:
487
+
488
+ ```ts
489
+ // test/nuxt/post-list.spec.ts
490
+ import { mountSuspended } from "@nuxt/test-utils/runtime"
491
+ import { expect, test } from "vitest"
492
+
493
+ import PostList from "~/components/post-list.vue"
494
+
495
+ import { procedures } from "./setup"
496
+
497
+ test("renders the posts", async () => {
498
+ const list = procedures.blog.posts.list.handle(() => [{ id: 1, title: "First post" }])
499
+ const component = await mountSuspended(PostList)
500
+
501
+ expect(component.text()).toContain("First post")
502
+ expect(list).toHaveBeenCalledOnce()
503
+ })
504
+ ```
505
+
416
506
  ### Outside Vue components
417
507
 
418
- You can call `.useQuery()` and `.useMutation()` outside a component, for example in tests.
508
+ You can call `.useQuery()` and `.useMutation()` outside a component, for example in a script or in a test that never mounts one.
419
509
  Create them inside `scope.run()` so Vue can track their reactive subscriptions, then call `scope.stop()` when you are done.
420
510
  When Vue injection is unavailable, pass a QueryClient explicitly:
421
511
 
@@ -444,9 +534,12 @@ try {
444
534
 
445
535
  Install dependencies with `bun install`, then run `bun run build`, `bun run types`, and `bun run test`.
446
536
 
537
+ Run `bun run test:component` to check the documented component-test recipe in the fixture application; it uses the built package, so build first.
538
+
447
539
  To try the package in a Nuxt app, build it and run `bunx nuxt dev tests/fixtures/nuxt`.
448
540
  The example app uses the built package, so rebuild after changing its source.
449
541
 
450
- Run `bunx playwright install chromium`, then `bun run test:nuxt` to check the packed npm archive with the oldest and newest supported Nuxt 3 and Nuxt 4 releases.
451
- Each version is checked with both module-managed and app-managed QueryClients, including types, SSR, and hydration in development and production.
452
- To check one version, use `bun run test:nuxt 3.14.1592`.
542
+ 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.
543
+ It is checked with both module-managed and app-managed QueryClients, including types, SSR, and hydration in development and production.
544
+
545
+ 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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.14.1592 || ^4.0.1"
6
6
  },
7
- "version": "0.1.1",
7
+ "version": "0.3.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -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,34 @@
1
+ import type { AnyNestedClient, Client } from "@orpc/client";
2
+ import type { Mock } from "vitest";
3
+ import type { ORPCNuxtClient, ORPCNuxtClientOptions } from "./types.js";
4
+ type MaybePromise<T> = T | Promise<T>;
5
+ /** A procedure implementation registered for a test client. */
6
+ export type TestORPCHandler<TProcedure> = TProcedure extends Client<infer _Context, infer Input, infer Output, infer _Error> ? (input: Input) => MaybePromise<Awaited<Output>> : never;
7
+ /** Registration methods for one procedure in a test client. */
8
+ export interface TestORPCProcedure<TProcedure> {
9
+ /** Register the implementation used by subsequent calls and return its Vitest mock. */
10
+ handle(handler: TestORPCHandler<TProcedure>): Mock<TestORPCHandler<TProcedure>>;
11
+ }
12
+ /** A router-shaped tree whose procedure leaves register test implementations. */
13
+ export type TestORPCProcedures<T extends AnyNestedClient> = T extends Client<infer Context, infer Input, infer Output, infer Error> ? TestORPCProcedure<Client<Context, Input, Output, Error>> : {
14
+ [Key in keyof T]: T[Key] extends AnyNestedClient ? TestORPCProcedures<T[Key]> : never;
15
+ };
16
+ /** The isolated client, procedure registry and cleanup function created for a test suite. */
17
+ export interface TestORPCClient<T extends AnyNestedClient> {
18
+ /** The decorated client to return from a mocked `useOrpc()`. */
19
+ client: ORPCNuxtClient<T>;
20
+ /** Register typed implementations and receive Vitest mocks for call assertions. */
21
+ procedures: TestORPCProcedures<T>;
22
+ /** Remove every registered procedure implementation. */
23
+ reset: () => void;
24
+ }
25
+ /**
26
+ * Create an isolated fake oRPC client for Nuxt component tests.
27
+ * The entrypoint has no Nuxt runtime dependency.
28
+ * A shared setup file can therefore import it when Vitest hoists `mockNuxtImport()`.
29
+ *
30
+ * @param options - Cache key prefix and an optional test-owned QueryClient.
31
+ * @returns A decorated client, its typed registration tree and a registration reset function.
32
+ */
33
+ export declare function createTestORPCClient<T extends AnyNestedClient>(options?: ORPCNuxtClientOptions): TestORPCClient<T>;
34
+ export {};
@@ -0,0 +1,60 @@
1
+ import { createORPCClient } from "@orpc/client";
2
+ import { vi } from "vitest";
3
+ import { createORPCNuxtClient } from "./client/create.js";
4
+ export function createTestORPCClient(options = {}) {
5
+ const registrations = /* @__PURE__ */ new Map();
6
+ function registerHandler(path, handler) {
7
+ const mock = vi.fn(handler);
8
+ registrations.set(path, mock);
9
+ return mock;
10
+ }
11
+ async function callHandler(path, input) {
12
+ const handler = registrations.get(path);
13
+ if (!handler) {
14
+ throw new Error(`No test handler is registered for oRPC procedure "${path}"`);
15
+ }
16
+ return await handler(input);
17
+ }
18
+ const link = {
19
+ async call(path, input) {
20
+ return await callHandler(path.join("."), input);
21
+ }
22
+ };
23
+ const client = createORPCNuxtClient(createORPCClient(link), options);
24
+ const procedures = createRecursiveProxy([], (path, args) => {
25
+ if (path.at(-1) !== "handle") {
26
+ throw new Error(`Unknown test procedure call: ${path.join(".")}`);
27
+ }
28
+ const handler = args[0];
29
+ if (typeof handler !== "function") {
30
+ throw new TypeError(
31
+ `A test handler must be a function for oRPC procedure "${path.slice(0, -1).join(".")}"`
32
+ );
33
+ }
34
+ return registerHandler(path.slice(0, -1).join("."), handler);
35
+ });
36
+ return {
37
+ client,
38
+ procedures,
39
+ reset: () => registrations.clear()
40
+ };
41
+ }
42
+ function createRecursiveProxy(path, call) {
43
+ const target = () => {
44
+ };
45
+ const children = /* @__PURE__ */ new Map();
46
+ return new Proxy(target, {
47
+ get(_, property, receiver) {
48
+ if (typeof property !== "string" || property === "then") {
49
+ return Reflect.get(target, property, receiver);
50
+ }
51
+ if (children.has(property)) return children.get(property);
52
+ const child = createRecursiveProxy([...path, property], call);
53
+ children.set(property, child);
54
+ return child;
55
+ },
56
+ apply(_, __, args) {
57
+ return call(path, args);
58
+ }
59
+ });
60
+ }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "orpc-nuxt",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
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",
@@ -33,6 +33,10 @@
33
33
  "types": "./dist/runtime/plugin.d.ts",
34
34
  "import": "./dist/runtime/plugin.js"
35
35
  },
36
+ "./testing": {
37
+ "types": "./dist/runtime/testing.d.ts",
38
+ "import": "./dist/runtime/testing.js"
39
+ },
36
40
  "./package.json": "./package.json"
37
41
  },
38
42
  "publishConfig": {
@@ -46,6 +50,7 @@
46
50
  "prepare": "lefthook install",
47
51
  "prepublishOnly": "bun run build",
48
52
  "test": "bun test src tests/runtime",
53
+ "test:component": "vitest run --root tests/fixtures/nuxt",
49
54
  "test:nuxt": "bun tests/nuxt/run.ts",
50
55
  "types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json && nuxt typecheck tests/fixtures/nuxt"
51
56
  },
@@ -58,25 +63,35 @@
58
63
  "@changesets/cli": "^2.31.1",
59
64
  "@nuxt/cli": "^3.37.0",
60
65
  "@nuxt/module-builder": "^1.0.3",
66
+ "@nuxt/test-utils": "^4.3.2",
61
67
  "@orpc/server": "2.0.0-beta.35",
62
68
  "@playwright/test": "1.58.2",
63
69
  "@tsconfig/bun": "^1.0.10",
64
70
  "@types/bun": "^1.3.14",
65
71
  "@vue/server-renderer": "^3.5.0",
72
+ "@vue/test-utils": "^2.5.0",
73
+ "happy-dom": "^20.14.3",
66
74
  "nuxt": "^4.5.2",
67
75
  "oxfmt": "^0.67.0",
68
76
  "oxlint": "^1.82.0",
69
77
  "publint": "^0.3.22",
70
78
  "typescript": "^5.9.3",
79
+ "vitest": "^5.0.0",
71
80
  "vue-tsc": "^3.3.11",
72
81
  "zod": "^4.3.6"
73
82
  },
74
83
  "peerDependencies": {
75
- "@orpc/client": "2.0.0-beta.35",
76
- "@orpc/tanstack-query": "2.0.0-beta.35",
84
+ "@orpc/client": "^2.0.0-beta.35",
85
+ "@orpc/tanstack-query": "^2.0.0-beta.35",
77
86
  "@tanstack/vue-query": "^5.102.8",
78
87
  "nuxt": "^3.14.1592 || ^4.0.1",
88
+ "vitest": "^4.0.0 || ^5.0.0",
79
89
  "vue": "^3.5.0"
80
90
  },
91
+ "peerDependenciesMeta": {
92
+ "vitest": {
93
+ "optional": true
94
+ }
95
+ },
81
96
  "packageManager": "bun@1.3.14"
82
97
  }