orpc-nuxt 0.2.0 → 0.3.1

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
@@ -432,66 +432,65 @@ const orpc = createORPCNuxtClient(client, { prefix: "blog" })
432
432
 
433
433
  ### Component tests
434
434
 
435
- Component tests need the Nuxt environment of `@nuxt/test-utils`:
435
+ Configure the Nuxt test environment and load a shared setup file:
436
436
 
437
437
  ```ts
438
438
  // vitest.config.ts
439
439
  import { defineVitestConfig } from "@nuxt/test-utils/config"
440
440
 
441
- export default defineVitestConfig({})
441
+ export default defineVitestConfig({
442
+ test: {
443
+ setupFiles: ["test/nuxt/setup.ts"],
444
+ },
445
+ })
442
446
  ```
443
447
 
444
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.
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.
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.
447
452
 
448
453
  ```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
+ // test/nuxt/setup.ts
455
+ import { mockNuxtImport } from "@nuxt/test-utils/runtime"
456
+ import type { RouterClient } from "@orpc/server"
457
+ import { createTestORPCClient } from "orpc-nuxt/testing"
458
+ import { afterEach } from "vitest"
454
459
 
455
- import PostList from "~/components/post-list.vue"
460
+ import type { router } from "~~/server/rpc/router"
456
461
 
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
- })
462
+ export const { client, procedures, reset } = createTestORPCClient<RouterClient<typeof router>>()
461
463
 
462
- const orpc = createORPCNuxtClient(
463
- {
464
- blog: {
465
- posts: {
466
- list: async () => [{ id: 1, title: "First post" }],
467
- },
468
- },
469
- },
470
- { queryClient },
471
- )
464
+ // Return the composable itself; Vitest hoists this factory before the setup file runs.
465
+ mockNuxtImport("useOrpc", () => () => client)
472
466
 
473
- // Vitest runs this factory before the file body, so it must return the composable without calling it.
474
- mockNuxtImport("useOrpc", () => () => orpc)
467
+ afterEach(reset)
468
+ ```
475
469
 
476
- afterEach(() => {
477
- // Start every test from an empty cache, so an earlier response cannot satisfy a later query.
478
- queryClient.clear()
479
- })
470
+ `orpc-nuxt/testing` does not import `nuxt/app`, so the hoisted `mockNuxtImport()` factory can load it safely.
471
+ The client owns a QueryClient that never retries, so a failing procedure fails the test instead of timing out, and `reset()` removes the registered handlers together with the cached responses.
472
+ Reach that cache as `queryClient` to seed or inspect it, or pass your own to `createTestORPCClient()`; `reset()` clears that one as well.
473
+
474
+ Register the required handlers before mounting; `mountSuspended()` waits for awaited queries before assertions:
475
+
476
+ ```ts
477
+ // test/nuxt/post-list.spec.ts
478
+ import { mountSuspended } from "@nuxt/test-utils/runtime"
479
+ import { expect, test } from "vitest"
480
+
481
+ import PostList from "~/components/post-list.vue"
482
+
483
+ import { procedures } from "./setup"
480
484
 
481
485
  test("renders the posts", async () => {
486
+ const list = procedures.blog.posts.list.handle(() => [{ id: 1, title: "First post" }])
482
487
  const component = await mountSuspended(PostList)
488
+
483
489
  expect(component.text()).toContain("First post")
490
+ expect(list).toHaveBeenCalledOnce()
484
491
  })
485
492
  ```
486
493
 
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
-
495
494
  ### Outside Vue components
496
495
 
497
496
  You can call `.useQuery()` and `.useMutation()` outside a component, for example in a script or in a test that never mounts one.
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.2.0",
7
+ "version": "0.3.1",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
@@ -0,0 +1,40 @@
1
+ import type { AnyNestedClient, Client } from "@orpc/client";
2
+ import { QueryClient } from "@tanstack/vue-query";
3
+ import type { Mock } from "vitest";
4
+ import type { ORPCNuxtClient, ORPCNuxtClientOptions } from "./types.js";
5
+ type MaybePromise<T> = T | Promise<T>;
6
+ /** A procedure implementation registered for a test client. */
7
+ export type TestORPCHandler<TProcedure> = TProcedure extends Client<infer _Context, infer Input, infer Output, infer _Error> ? (input: Input) => MaybePromise<Awaited<Output>> : never;
8
+ /** Registration methods for one procedure in a test client. */
9
+ export interface TestORPCProcedure<TProcedure> {
10
+ /** Register the implementation used by subsequent calls and return its Vitest mock. */
11
+ handle(handler: TestORPCHandler<TProcedure>): Mock<TestORPCHandler<TProcedure>>;
12
+ }
13
+ /** A router-shaped tree whose procedure leaves register test implementations. */
14
+ export type TestORPCProcedures<T extends AnyNestedClient> = T extends Client<infer Context, infer Input, infer Output, infer Error> ? TestORPCProcedure<Client<Context, Input, Output, Error>> : {
15
+ [Key in keyof T]: T[Key] extends AnyNestedClient ? TestORPCProcedures<T[Key]> : never;
16
+ };
17
+ /** The isolated client, procedure registry and cleanup function created for a test suite. */
18
+ export interface TestORPCClient<T extends AnyNestedClient> {
19
+ /** The decorated client to return from a mocked `useOrpc()`. */
20
+ client: ORPCNuxtClient<T>;
21
+ /** Register typed implementations and receive Vitest mocks for call assertions. */
22
+ procedures: TestORPCProcedures<T>;
23
+ /** The cache the client reads and writes, to seed or inspect from a test. */
24
+ queryClient: QueryClient;
25
+ /** Remove every registered procedure implementation and clear the query cache. */
26
+ reset: () => void;
27
+ }
28
+ /**
29
+ * Create an isolated fake oRPC client for Nuxt component tests.
30
+ * The entrypoint has no Nuxt runtime dependency.
31
+ * A shared setup file can therefore import it when Vitest hoists `mockNuxtImport()`.
32
+ *
33
+ * Without a `queryClient` the client owns one that never retries, so a failing procedure fails
34
+ * the test instead of retrying until it times out.
35
+ *
36
+ * @param options - Cache key prefix and a QueryClient to use instead of the owned one.
37
+ * @returns A decorated client, its typed registration tree, its cache and a reset function.
38
+ */
39
+ export declare function createTestORPCClient<T extends AnyNestedClient>(options?: ORPCNuxtClientOptions): TestORPCClient<T>;
40
+ export {};
@@ -0,0 +1,71 @@
1
+ import { createORPCClient } from "@orpc/client";
2
+ import { QueryClient } from "@tanstack/vue-query";
3
+ import { vi } from "vitest";
4
+ import { createORPCNuxtClient } from "./client/create.js";
5
+ export function createTestORPCClient(options = {}) {
6
+ const registrations = /* @__PURE__ */ new Map();
7
+ const queryClient = options.queryClient ?? new QueryClient({
8
+ defaultOptions: {
9
+ // Retries turn a failing procedure into a test timeout instead of a reported failure.
10
+ queries: { retry: false }
11
+ }
12
+ });
13
+ function registerHandler(path, handler) {
14
+ const mock = vi.fn(handler);
15
+ registrations.set(path, mock);
16
+ return mock;
17
+ }
18
+ async function callHandler(path, input) {
19
+ const handler = registrations.get(path);
20
+ if (!handler) {
21
+ throw new Error(`No test handler is registered for oRPC procedure "${path}"`);
22
+ }
23
+ return await handler(input);
24
+ }
25
+ const link = {
26
+ async call(path, input) {
27
+ return await callHandler(path.join("."), input);
28
+ }
29
+ };
30
+ const client = createORPCNuxtClient(createORPCClient(link), { ...options, queryClient });
31
+ const procedures = createRecursiveProxy([], (path, args) => {
32
+ if (path.at(-1) !== "handle") {
33
+ throw new Error(`Unknown test procedure call: ${path.join(".")}`);
34
+ }
35
+ const handler = args[0];
36
+ if (typeof handler !== "function") {
37
+ throw new TypeError(
38
+ `A test handler must be a function for oRPC procedure "${path.slice(0, -1).join(".")}"`
39
+ );
40
+ }
41
+ return registerHandler(path.slice(0, -1).join("."), handler);
42
+ });
43
+ return {
44
+ client,
45
+ procedures,
46
+ queryClient,
47
+ reset: () => {
48
+ registrations.clear();
49
+ queryClient.clear();
50
+ }
51
+ };
52
+ }
53
+ function createRecursiveProxy(path, call) {
54
+ const target = () => {
55
+ };
56
+ const children = /* @__PURE__ */ new Map();
57
+ return new Proxy(target, {
58
+ get(_, property, receiver) {
59
+ if (typeof property !== "string" || property === "then") {
60
+ return Reflect.get(target, property, receiver);
61
+ }
62
+ if (children.has(property)) return children.get(property);
63
+ const child = createRecursiveProxy([...path, property], call);
64
+ children.set(property, child);
65
+ return child;
66
+ },
67
+ apply(_, __, args) {
68
+ return call(path, args);
69
+ }
70
+ });
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orpc-nuxt",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
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": {
@@ -81,7 +85,13 @@
81
85
  "@orpc/tanstack-query": "^2.0.0-beta.35",
82
86
  "@tanstack/vue-query": "^5.102.8",
83
87
  "nuxt": "^3.14.1592 || ^4.0.1",
88
+ "vitest": "^4.0.0 || ^5.0.0",
84
89
  "vue": "^3.5.0"
85
90
  },
91
+ "peerDependenciesMeta": {
92
+ "vitest": {
93
+ "optional": true
94
+ }
95
+ },
86
96
  "packageManager": "bun@1.3.14"
87
97
  }