orpc-nuxt 0.4.0 → 0.6.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
@@ -171,6 +171,39 @@ Use `.call()` when you just need a procedure's response, without query state or
171
171
  const post = await orpc.blog.posts.get.call({ id: 1 })
172
172
  ```
173
173
 
174
+ ### Declared errors
175
+
176
+ Treat an expected rejection as an application outcome only when the procedure declares it with `.errors()`.
177
+ Let every other error propagate to the application's error handler, including an `ORPCError` whose code was not declared by that procedure.
178
+
179
+ Use `catchORPCError()` from the client entrypoint to handle selected declared errors while preserving their code and data types:
180
+
181
+ ```ts
182
+ import { catchORPCError } from "orpc-nuxt/client"
183
+
184
+ const post = await catchORPCError(orpc.blog.posts.update.call(input), {
185
+ CONFLICT: (error) => {
186
+ message.value = error.message
187
+ conflictingField.value = error.data.field
188
+ },
189
+ NOT_FOUND: () => navigateTo("/posts"),
190
+ })
191
+ ```
192
+
193
+ The successful output, matching handler's awaited result, or matching non-function value becomes the call result.
194
+ Undeclared errors and declared codes without a handler are rethrown unchanged.
195
+
196
+ When one declared error simply means there is no result, map it to `undefined` or `null` directly:
197
+
198
+ ```ts
199
+ import { catchORPCError } from "orpc-nuxt/client"
200
+
201
+ const post = await catchORPCError(orpc.blog.posts.get.call({ id }), {
202
+ NOT_FOUND: null,
203
+ })
204
+ // post is the procedure output or null.
205
+ ```
206
+
174
207
  The client also exposes oRPC's utilities:
175
208
 
176
209
  - `.key()` builds a cache key prefix for a procedure or router branch.
@@ -524,6 +557,20 @@ test("renders the posts", async () => {
524
557
  })
525
558
  ```
526
559
 
560
+ The second handler argument provides typed error constructors derived from that procedure's `.errors()` map:
561
+
562
+ ```ts
563
+ procedures.blog.posts.update.handle((input, { errors }) => {
564
+ if (input.title === "taken") {
565
+ throw errors.CONFLICT({
566
+ message: "Already exists",
567
+ data: { field: "title" },
568
+ })
569
+ }
570
+ return { id: 1, ...input }
571
+ })
572
+ ```
573
+
527
574
  ### Outside Vue components
528
575
 
529
576
  You can call `.useQuery()` and `.useMutation()` outside a component, for example in a script or in a test that never mounts one.
@@ -553,14 +600,16 @@ try {
553
600
 
554
601
  ## Development
555
602
 
556
- Install dependencies with `bun install`, then run `bun run build`, `bun run types`, and `bun run test`.
603
+ Install dependencies from the repository root with `bun install`.
604
+ Run `bun run build`, `bun run types`, and `bun run test` from this package's directory.
557
605
 
558
606
  Run `bun run test:component` to check the documented component-test recipe in the fixture application; it uses the built package, so build first.
559
607
 
560
608
  To try the package in a Nuxt app, build it and run `bunx nuxt dev tests/fixtures/nuxt`.
561
609
  The example app uses the built package, so rebuild after changing its source.
562
610
 
563
- 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.
611
+ From the repository root, install the shared test browser with `bunx playwright install chromium-headless-shell`.
612
+ Then run `bun run test:nuxt` from this package's directory to check the packed npm archive with the Nuxt version installed in the workspace.
564
613
  It is checked with both module-managed and app-managed QueryClients, including types, SSR, and hydration in development and production.
565
614
 
566
615
  To check other Nuxt versions by hand, pass them explicitly: `bun run test:nuxt 3.14.1592 4.0.1`.
package/dist/module.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import * as _nuxt_schema from '@nuxt/schema';
1
+ import * as nuxt_schema from 'nuxt/schema';
2
2
  import { StaticQueryClientConfig } from '../dist/runtime/nuxt/query-config.js';
3
3
  export { StaticQueryClientConfig } from '../dist/runtime/nuxt/query-config.js';
4
4
  export { ORPCRuntimeHooks as ModuleRuntimeHooks } from '../dist/runtime/nuxt/hooks.js';
@@ -8,7 +8,7 @@ interface ModuleOptions {
8
8
  /** Set static query defaults, or use false when another plugin owns Vue Query and hydration. */
9
9
  queryClient?: boolean | StaticQueryClientConfig;
10
10
  }
11
- declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
11
+ declare const _default: nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
12
12
 
13
13
  export { _default as default };
14
14
  export type { ModuleOptions };
package/dist/module.json CHANGED
@@ -4,9 +4,9 @@
4
4
  "compatibility": {
5
5
  "nuxt": "^3.14.1592 || ^4.0.1"
6
6
  },
7
- "version": "0.4.0",
7
+ "version": "0.6.0",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
- "unbuild": "3.6.1"
10
+ "unbuild": "unknown"
11
11
  }
12
12
  }
@@ -0,0 +1,32 @@
1
+ import { type AnyORPCError } from "@orpc/client";
2
+ type ErrorOf<TPromise extends Promise<unknown>> = TPromise extends {
3
+ __error?: {
4
+ type: infer Error;
5
+ };
6
+ } ? Error : never;
7
+ type DefinedError<TPromise extends Promise<unknown>> = Extract<ErrorOf<TPromise>, AnyORPCError>;
8
+ type DefinedErrorCode<TPromise extends Promise<unknown>> = DefinedError<TPromise>["code"] & string;
9
+ type ErrorResultValue = string | number | boolean | bigint | symbol | null | undefined | readonly unknown[] | {
10
+ readonly [key: string]: unknown;
11
+ };
12
+ type DefinedErrorHandlers<TPromise extends Promise<unknown>> = Partial<{
13
+ [Code in DefinedErrorCode<TPromise>]: ((error: Extract<DefinedError<TPromise>, {
14
+ code: Code;
15
+ }>) => unknown) | ErrorResultValue;
16
+ }>;
17
+ type HandledResult<Handlers> = {
18
+ [Code in keyof Handlers]: Handlers[Code] extends (...args: never[]) => infer Result ? Awaited<Result> : Awaited<Handlers[Code]>;
19
+ }[keyof Handlers];
20
+ /**
21
+ * Handle selected errors declared by an oRPC procedure and rethrow every other rejection.
22
+ * The promise must come directly from a typed client call so its declared error union is preserved.
23
+ * Each handler receives the declared error branch narrowed to its own code.
24
+ * A non-function value is returned directly when its error code matches.
25
+ * Synchronous and asynchronous handler results are included in the returned promise type.
26
+ *
27
+ * @param promise - The `PromiseWithError` returned by an oRPC client procedure.
28
+ * @param handlers - Handlers keyed by declared error code.
29
+ * @returns The procedure output or the result of the matching handler.
30
+ */
31
+ export declare function catchORPCError<TPromise extends Promise<unknown>, Handlers extends object & DefinedErrorHandlers<NoInfer<TPromise>>>(promise: TPromise, handlers: "__error" extends keyof TPromise ? Handlers & Record<Exclude<keyof Handlers, DefinedErrorCode<NoInfer<TPromise>>>, never> : never): Promise<Awaited<TPromise> | HandledResult<Handlers>>;
32
+ export {};
@@ -0,0 +1,10 @@
1
+ import { isDefinedError } from "@orpc/client";
2
+ export function catchORPCError(promise, handlers) {
3
+ return promise.catch((error) => {
4
+ const orpcError = error;
5
+ if (!isDefinedError(orpcError)) throw error;
6
+ if (!Object.hasOwn(handlers, orpcError.code)) throw error;
7
+ const handler = handlers[orpcError.code];
8
+ return typeof handler === "function" ? handler(orpcError) : handler;
9
+ });
10
+ }
@@ -1,2 +1,3 @@
1
1
  export { createORPCNuxtClient } from "./client/create.js";
2
+ export { catchORPCError } from "./client/error.js";
2
3
  export type { AwaitableQuery, ORPCMutationOptions, ORPCNuxtClient, ORPCNuxtClientOptions, ORPCQueryOptions, ORPCQueryResult, ORPCSelectedQueryResult, } from "./types.js";
@@ -1 +1,2 @@
1
1
  export { createORPCNuxtClient } from "./client/create.js";
2
+ export { catchORPCError } from "./client/error.js";
@@ -1,14 +1,35 @@
1
- import type { AnyNestedClient, Client } from "@orpc/client";
1
+ import type { AnyORPCError, AnyNestedClient, Client } from "@orpc/client";
2
+ import { ORPCError } from "@orpc/client";
2
3
  import { QueryClient } from "@tanstack/vue-query";
3
4
  import type { Mock } from "vitest";
4
5
  import type { ORPCNuxtClient, ORPCNuxtClientOptions } from "./types.js";
5
6
  type MaybePromise<T> = T | Promise<T>;
7
+ type ProcedureError<TProcedure> = TProcedure extends Client<any, any, any, infer Error> ? Extract<Error, AnyORPCError> : never;
8
+ type ErrorConstructorOptions<Data> = ErrorOptions & {
9
+ message?: string;
10
+ } & (undefined extends Data ? {
11
+ data?: Data;
12
+ } : {
13
+ data: Data;
14
+ });
15
+ type ErrorConstructor<ErrorType extends AnyORPCError> = ErrorType extends ORPCError<infer _Code, infer Data> ? (...args: undefined extends Data ? [options?: ErrorConstructorOptions<Data>] : [options: ErrorConstructorOptions<Data>]) => ErrorType : never;
16
+ type TestORPCErrors<TProcedure> = {
17
+ [Code in ProcedureError<TProcedure>["code"] & string]: ErrorConstructor<Extract<ProcedureError<TProcedure>, {
18
+ code: Code;
19
+ }>>;
20
+ };
21
+ /** Error constructors derived from the procedure's declared error union. */
22
+ export interface TestORPCHandlerOptions<TProcedure> {
23
+ /** Construct an error declared by this procedure, with its corresponding data type. */
24
+ errors: TestORPCErrors<TProcedure>;
25
+ }
6
26
  /** 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;
27
+ export type TestORPCHandler<TProcedure> = TProcedure extends Client<infer _Context, infer Input, infer Output, infer _Error> ? (input: Input, options: TestORPCHandlerOptions<TProcedure>) => MaybePromise<Awaited<Output>> : never;
28
+ type TestORPCMockHandler<TProcedure> = TProcedure extends Client<infer _Context, infer Input, infer Output, infer _Error> ? (input: Input) => MaybePromise<Awaited<Output>> : never;
8
29
  /** Registration methods for one procedure in a test client. */
9
30
  export interface TestORPCProcedure<TProcedure> {
10
31
  /** Register the implementation used by subsequent calls and return its Vitest mock. */
11
- handle(handler: TestORPCHandler<TProcedure>): Mock<TestORPCHandler<TProcedure>>;
32
+ handle(handler: TestORPCHandler<TProcedure>): Mock<TestORPCMockHandler<TProcedure>>;
12
33
  }
13
34
  /** A router-shaped tree whose procedure leaves register test implementations. */
14
35
  export type TestORPCProcedures<T extends AnyNestedClient> = T extends Client<infer Context, infer Input, infer Output, infer Error> ? TestORPCProcedure<Client<Context, Input, Output, Error>> : {
@@ -1,4 +1,4 @@
1
- import { createORPCClient } from "@orpc/client";
1
+ import { createORPCClient, createORPCErrorFromJson, ORPCError } from "@orpc/client";
2
2
  import { QueryClient } from "@tanstack/vue-query";
3
3
  import { vi } from "vitest";
4
4
  import { createORPCNuxtClient } from "./client/create.js";
@@ -11,7 +11,8 @@ export function createTestORPCClient(options = {}) {
11
11
  }
12
12
  });
13
13
  function registerHandler(path, handler) {
14
- const mock = vi.fn(handler);
14
+ const options2 = { errors: createErrorConstructors() };
15
+ const mock = vi.fn((input) => handler(input, options2));
15
16
  registrations.set(path, mock);
16
17
  return mock;
17
18
  }
@@ -50,6 +51,22 @@ export function createTestORPCClient(options = {}) {
50
51
  }
51
52
  };
52
53
  }
54
+ function createErrorConstructors() {
55
+ const constructors = /* @__PURE__ */ new Map();
56
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
57
+ get(target, property, receiver) {
58
+ if (typeof property !== "string") return Reflect.get(target, property, receiver);
59
+ const cached = constructors.get(property);
60
+ if (cached) return cached;
61
+ const constructor = (options) => {
62
+ const error = new ORPCError(property, options);
63
+ return createORPCErrorFromJson({ ...error.toJSON(), defined: true }, { cause: error.cause });
64
+ };
65
+ constructors.set(property, constructor);
66
+ return constructor;
67
+ }
68
+ });
69
+ }
53
70
  function createRecursiveProxy(path, call) {
54
71
  const target = () => {
55
72
  };
@@ -8,4 +8,4 @@ import { type QueryClient } from "@tanstack/vue-query";
8
8
  * @param options - Mutation options, optionally wrapped in a ref or getter.
9
9
  * @param queryClient - An explicit cache owner; otherwise Vue Query uses the injected client.
10
10
  */
11
- export declare function useORPCMutation(target: object, options: unknown, queryClient?: QueryClient): import("@tanstack/vue-query").UseMutationReturnType<unknown, Error, unknown, unknown, Omit<import("@tanstack/query-core").MutationObserverIdleResult<unknown, Error, unknown, unknown>, "mutate" | "reset"> | Omit<import("@tanstack/query-core").MutationObserverLoadingResult<unknown, Error, unknown, unknown>, "mutate" | "reset"> | Omit<import("@tanstack/query-core").MutationObserverErrorResult<unknown, Error, unknown, unknown>, "mutate" | "reset"> | Omit<import("@tanstack/query-core").MutationObserverSuccessResult<unknown, Error, unknown, unknown>, "mutate" | "reset">>;
11
+ export declare function useORPCMutation(target: object, options: unknown, queryClient?: QueryClient): import("@tanstack/vue-query").UseMutationReturnType<unknown, Error, unknown, unknown, Omit<import("@tanstack/vue-query").MutationObserverIdleResult<unknown, Error, unknown, unknown>, "mutate" | "reset"> | Omit<import("@tanstack/vue-query").MutationObserverLoadingResult<unknown, Error, unknown, unknown>, "mutate" | "reset"> | Omit<import("@tanstack/vue-query").MutationObserverErrorResult<unknown, Error, unknown, unknown>, "mutate" | "reset"> | Omit<import("@tanstack/vue-query").MutationObserverSuccessResult<unknown, Error, unknown, unknown>, "mutate" | "reset">>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orpc-nuxt",
3
- "version": "0.4.0",
3
+ "version": "0.6.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",
@@ -8,7 +8,8 @@
8
8
  "author": "Ilya Semenov",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/IlyaSemenov/orpc-nuxt.git"
11
+ "url": "git+https://github.com/IlyaSemenov/orpc-nuxt.git",
12
+ "directory": "packages/orpc-nuxt"
12
13
  },
13
14
  "files": [
14
15
  "README.md",
@@ -45,14 +46,11 @@
45
46
  },
46
47
  "scripts": {
47
48
  "build": "nuxt-module-build build && publint",
48
- "lint": "oxlint --fix && oxfmt",
49
- "lint:check": "oxlint && oxfmt --check",
50
- "prepare": "lefthook install",
51
- "prepublishOnly": "bun run build",
52
49
  "test": "bun test src tests/runtime",
53
50
  "test:component": "vitest run --root tests/fixtures/nuxt",
54
51
  "test:nuxt": "bun tests/nuxt/run.ts",
55
- "types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json && nuxt typecheck tests/fixtures/nuxt"
52
+ "types": "tsc --noEmit && tsc --noEmit -p tests/tsconfig.json && nuxt typecheck tests/fixtures/nuxt",
53
+ "prepublishOnly": "bun run build"
56
54
  },
57
55
  "dependencies": {
58
56
  "@nuxt/kit": "^3.14.1592 || ^4.0.1",
@@ -60,23 +58,23 @@
60
58
  "es-toolkit": "^1.52.0"
61
59
  },
62
60
  "devDependencies": {
63
- "@changesets/cli": "^2.31.1",
64
61
  "@nuxt/cli": "^3.37.0",
65
62
  "@nuxt/module-builder": "^1.0.3",
66
63
  "@nuxt/test-utils": "^4.3.2",
64
+ "@orpc/client": "^2.0.0-beta.35",
67
65
  "@orpc/server": "2.0.0-beta.35",
68
- "@playwright/test": "1.58.2",
66
+ "@orpc/tanstack-query": "^2.0.0-beta.35",
67
+ "@tanstack/vue-query": "^5.102.8",
69
68
  "@tsconfig/bun": "^1.0.10",
70
69
  "@types/bun": "^1.3.14",
71
70
  "@vue/server-renderer": "^3.5.0",
72
71
  "@vue/test-utils": "^2.5.0",
73
72
  "happy-dom": "^20.14.3",
74
73
  "nuxt": "^4.5.2",
75
- "oxfmt": "^0.67.0",
76
- "oxlint": "^1.82.0",
77
74
  "publint": "^0.3.22",
78
75
  "typescript": "^5.9.3",
79
76
  "vitest": "^5.0.0",
77
+ "vue": "^3.5.0",
80
78
  "vue-tsc": "^3.3.11",
81
79
  "zod": "^4.3.6"
82
80
  },
@@ -92,6 +90,5 @@
92
90
  "vitest": {
93
91
  "optional": true
94
92
  }
95
- },
96
- "packageManager": "bun@1.3.14"
93
+ }
97
94
  }