rpc4next 0.4.14 → 0.5.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 watanabe-1
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # rpc4next
2
+
3
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/watanabe-1/rpc4next)
4
+
5
+ Lightweight, type-safe RPC system for Next.js App Router projects.
6
+
7
+ Inspired by Hono RPC and Pathpida, **rpc4next** automatically generates a type-safe client for your existing `route.ts` **and** `page.tsx` files, enabling seamless server-client communication with full type inference.
8
+
9
+ ---
10
+
11
+ ## ✨ Features
12
+
13
+ - ✅ ルート、パラメータ、クエリパラメータ、 リクエストボディ、レスポンスの型安全なクライアント生成
14
+ - ✅ 既存の `app/**/route.ts` および `app/**/page.tsx` を活用するため、新たなハンドラファイルの作成は不要
15
+ - ✅ 最小限のセットアップで、カスタムサーバー不要
16
+ - ✅ 動的ルート(`[id]`、`[...slug]` など)に対応
17
+ - ✅ CLI による自動クライアント用型定義生成
18
+
19
+ ---
20
+
21
+ ## 🚀 Getting Started
22
+
23
+ ### 1. Install Packages
24
+
25
+ ```bash
26
+ npm install rpc4next
27
+ npm install -D rpc4next-cli
28
+ ```
29
+
30
+ ### 2. Define API Routes in Next.js
31
+
32
+ Next.js プロジェクト内の既存の `app/**/route.ts` と `app/**/page.tsx` ファイルをそのまま利用できます。
33
+ さらに、クエリパラメータ(searchParams)の型安全性を有効にするには、対象のファイル内で `Query` 型を定義し、`export` してください。
34
+
35
+ ```ts
36
+ // app/api/user/[id]/route.ts
37
+ import { NextRequest, NextResponse } from "next/server";
38
+
39
+ // searchParams用の型定義
40
+ export type Query = {
41
+ q: string; // 必須
42
+ page?: number; // 任意
43
+ };
44
+
45
+ export async function GET(
46
+ req: NextRequest,
47
+ segmentData: { params: Promise<{ id: string }> },
48
+ ) {
49
+ const { id } = await segmentData.params;
50
+ const q = req.nextUrl.searchParams.get("q");
51
+ return NextResponse.json({ id, q });
52
+ }
53
+ ```
54
+
55
+ 🚩 Query 型を export することで、searchParams の型も自動的にクライアントに反映されます。
56
+
57
+ - **RPCとしてresponseの戻り値の推論が機能するのは、対象となる `route.ts` の HTTPメソッドハンドラ内で`NextResponse.json()` をしている関数のみになります**
58
+
59
+ ---
60
+
61
+ ### 3. Generate Type Definitions with CLI
62
+
63
+ CLI を利用して、Next.js のルート構造から型安全な RPC クライアントの定義を自動生成します。
64
+
65
+ ```bash
66
+ npx rpc4next <baseDir> <outputPath>
67
+ ```
68
+
69
+ `rpc4next` command is provided by the `rpc4next-cli` package.
70
+
71
+ - `<baseDir>`: Next.js の Appルータが配置されたベースディレクトリ
72
+ - `<outputPath>`: 生成された型定義ファイルの出力先
73
+
74
+ #### オプション
75
+
76
+ - **ウォッチモード**
77
+ ファイル変更を検知して自動的に再生成する場合は `--watch` or `-w` オプションを付けます。
78
+
79
+ ```bash
80
+ npx rpc4next <baseDir> <outputPath> --watch
81
+ ```
82
+
83
+ - **パラメータ型ファイルの生成**
84
+ 各ルートに対して個別のパラメータ型定義ファイルを生成する場合は、`--params-file` or `-p` オプションにファイル名を指定します。
85
+
86
+ ```bash
87
+ npx rpc4next <baseDir> <outputPath> --params-file <paramsFileName>
88
+ ```
89
+
90
+ ---
91
+
92
+ ### 4. Create Your RPC Client
93
+
94
+ 生成された型定義ファイルを基に、RPC クライアントを作成します。
95
+
96
+ ```ts
97
+ // lib/rpcClient.ts
98
+ import { createClient } from "rpc4next/client";
99
+ import type { PathStructure } from "あなたが生成した型定義ファイル";
100
+
101
+ export const rpc = createClient<PathStructure>();
102
+ ```
103
+
104
+ ---
105
+
106
+ ### 5. Use It in Your Components
107
+
108
+ コンポーネント内で生成された RPC クライアントを使用します。
109
+
110
+ ```tsx
111
+ // app/page.tsx
112
+ import { rpc } from "@/lib/rpcClient";
113
+
114
+ export default async function Page() {
115
+ const res = await rpc.api.user._id("123").$get({
116
+ query: { q: "hello", page: 1 },
117
+ });
118
+ const json = await res.json();
119
+ return <div>{json.q}</div>;
120
+ }
121
+ ```
122
+
123
+ - エディタの補完機能により、利用可能なエンドポイントが自動的に表示されます。
124
+ - リクエストの構造(params, query)はサーバーコードから推論され、レスポンスも型安全に扱えます。
125
+
126
+ ---
127
+
128
+ ## ✅ さらに型安全にしたい場合 `createRouteHandler` による Next.js の型安全強化
129
+
130
+ ### 📌 主なメリット
131
+
132
+ 1. **レスポンス型安全**
133
+ - ステータス、Content-Type、Body がすべて型で保証される
134
+ - クライアントは受け取るレスポンス型を完全に推論可能
135
+
136
+ 2. **クライアント側補完強化**
137
+ - `status`, `content-type`, `json()`, `text()` などが適切に補完される
138
+
139
+ 3. **サーバー側 params / query も型安全**
140
+ - `createRouteHandler()` + `zValidator()` を使えば、`params`, `query`, `headers`, `cookies`, `json` も型推論・バリデーション可能
141
+ - `createRouteHandler()` + `zValidator()` を使えば、`Query` 型もexportする必要なし
142
+
143
+ ---
144
+
145
+ ### ✅ 基本的な使い方
146
+
147
+ ```ts
148
+ const createRouteHandler = routeHandlerFactory((err, rc) =>
149
+ rc.text("error", { status: 400 }),
150
+ );
151
+
152
+ const { POST } = createRouteHandler().post(async (rc) => rc.text("plain text"));
153
+ ```
154
+
155
+ これだけで、POSTリクエストの返り値に、レスポンスの内容 (`json`, `text`など)、`status`, `content-type` が型付けされるようになります。
156
+
157
+ ---
158
+
159
+ ### 👤 サーバー側でのより型安全なルート作成
160
+
161
+ `createRouteHandler()` と `zValidator()` を使うことで、各リクエストパーツに対して **型安全なバリデーション** をかけられます。
162
+
163
+ #### シンプルな例
164
+
165
+ ```ts
166
+ import { createRouteHandler } from "@/path/to/createRouteHandler";
167
+ import { zValidator } from "@/path/to/zValidator";
168
+ import { z } from "zod";
169
+
170
+ // Zodスキーマを定義
171
+ const paramsSchema = z.object({
172
+ userId: z.string(),
173
+ });
174
+
175
+ // バリデーション付きルートハンドラを作成
176
+ export const { GET } = createRouteHandler<{
177
+ params: z.infer<typeof paramsSchema>;
178
+ }>().get(
179
+ zValidator("params", paramsSchema), // paramsを検証
180
+ async (rc) => {
181
+ const params = rc.req.valid("params"); // バリデーション済みparamsを取得
182
+ return rc.json({ message: `User ID is ${params.userId}` });
183
+ },
184
+ );
185
+ ```
186
+
187
+ ## ✅ サポートされているバリデーションターゲット
188
+
189
+ サーバー側では,次のリクエスト部分を型安全に検証できます:
190
+
191
+ | ターゲット | 説明 |
192
+ | :--------- | :-------------------------------------------------- |
193
+ | `params` | URLパラメータ ( `/user/:id` の `id`など) |
194
+ | `query` | クエリパラメータ (`?q=xxx&page=1`など) |
195
+ | `headers` | リクエストヘッダー |
196
+ | `cookies` | クッキー |
197
+ | `json` | リクエストボディ (Content-Type: `application/json`) |
198
+
199
+ ---
200
+
201
+ ### 🔥 複数ターゲットを同時に検証する例
202
+
203
+ ```ts
204
+ import { createRouteHandler } from "@/path/to/createRouteHandler";
205
+ import { zValidator } from "@/path/to/zValidator";
206
+ import { z } from "zod";
207
+
208
+ const querySchema = z.object({
209
+ page: z.string().regex(/^\d+$/),
210
+ });
211
+
212
+ const jsonSchema = z.object({
213
+ name: z.string(),
214
+ age: z.number(),
215
+ });
216
+
217
+ export const { POST } = createRouteHandler<{
218
+ query: z.infer<typeof querySchema>;
219
+ }>().post(
220
+ zValidator("query", querySchema),
221
+ zValidator("json", jsonSchema),
222
+ async (rc) => {
223
+ const query = rc.req.valid("query");
224
+ const body = rc.req.valid("json");
225
+ return rc.json({ query, body });
226
+ },
227
+ );
228
+ ```
229
+
230
+ - `query`と`json`を別々のスキーマで検証
231
+ - **成功時は型安全に取得可能** (`rc.req.valid('query')`, `rc.req.valid('json')`)
232
+
233
+ ---
234
+
235
+ これにより,クライアント側とサーバー側が、全面的に**型でつながる**ので,ミスを何次も防げ,開発体験を大幅に向上できます。
236
+
237
+ ---
238
+
239
+ ### ⚡ バリデーション失敗時のカスタムエラーハンドリング
240
+
241
+ - デフォルトでは、バリデーション失敗時に自動で `400 Bad Request` を返します。
242
+ - 必要に応じて、**カスタムフック**でエラー対応を制御できます。
243
+
244
+ ```ts
245
+ zValidator("params", paramsSchema, (result, rc) => {
246
+ if (!result.success) {
247
+ return rc.json({ error: result.error.errors }, { status: 422 });
248
+ }
249
+ });
250
+ ```
251
+
252
+ > (フック内でレスポンスを返さない場合は、通常通り例外がスローされます)
253
+
254
+ ---
255
+
256
+ ## 📡 クライアント側での使い方
257
+
258
+ `rpc4next`で作成したクライアントは、`createRouteHandler` と `zValidator` で作成したルートハンドラの内容にしたがって **params, query, headers, cookies, json** を型安全に送信できます。
259
+
260
+ 例:
261
+
262
+ ```ts
263
+ import { createRpcClient } from "@/path/to/rpc-client";
264
+ import type { PathStructure } from "@/path/to/generated-types";
265
+
266
+ const client = createRpcClient<PathStructure>("http://localhost:3000");
267
+
268
+ async function callUserApi() {
269
+ const res = await client.api.menu.test.$post({
270
+ body: { json: { age: 20, name: "foo" } },
271
+ url: { query: { page: "1" } },
272
+ });
273
+
274
+ if (res.ok) {
275
+ const json = await res.json();
276
+
277
+ // ✅ 正常時は次の型が推論されます
278
+ // const json: {
279
+ // query: {
280
+ // page: string;
281
+ // };
282
+ // body: {
283
+ // name: string;
284
+ // age: number;
285
+ // };
286
+ // }
287
+ } else {
288
+ const error = await res.json();
289
+
290
+ // ⚠️ バリデーションエラー時は次の型が推論されます
291
+ // const error:
292
+ // | SafeParseError<{
293
+ // page: string;
294
+ // }>
295
+ // | SafeParseError<{
296
+ // name: string;
297
+ // age: number;
298
+ // }>;
299
+ }
300
+ }
301
+ ```
302
+
303
+ - エディタの補完機能により、送信できるターゲットが明示されます
304
+ - サーバー側の型定義に基づいて、**型のズレを防止**できます
305
+
306
+ ---
307
+
308
+ これらのように、リクエスト時にはさまざまなターゲット (`params`, `query`, `headers`, `cookies`, `json`) を送信できます。
309
+
310
+ さらに、サーバー側では、これらを**個別に型付け、バリデーション**できます。
311
+
312
+ ---
313
+
314
+ ## 🧭 Monorepo Layout
315
+
316
+ - `packages/rpc4next`: Core library modules (client, server, validators, shared types)
317
+ - `packages/rpc4next-cli`: CLI generator that exposes the `rpc4next` binary
318
+ - Install once at the repo root: `bun install`
319
+ - Build everything: `bun run build`
320
+ - Run all tests: `bun run test`
321
+ - Lint all packages: `bun run lint`
322
+
323
+ ## 🚧 Requirements
324
+
325
+ - Next.js 14+ (App Router 使用)
326
+ - Node.js 20.9.0+
327
+
328
+ ---
329
+
330
+ ## 💼 License
331
+
332
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export * from "./rpc/server";
2
1
  export * from "./rpc/client";
2
+ export * from "./rpc/server";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./rpc/server";export*from"./rpc/client";
1
+ export*from"./rpc/client";export*from"./rpc/server";
@@ -1 +1 @@
1
- import{DYNAMIC_PREFIX as i,CATCH_ALL_PREFIX as a,OPTIONAL_CATCH_ALL_PREFIX as p,HTTP_METHOD_FUNC_KEYS as u}from"rpc4next-shared";const T=t=>t.startsWith(i),l=t=>t.startsWith(a)||t.startsWith(p),y=new Set(u),A=t=>y.has(t),c=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),d=(t,n)=>{const s={...t};for(const e in n)if(Object.prototype.hasOwnProperty.call(n,e)){const r=t[e],o=n[e];c(r)&&c(o)?s[e]=d(r,o):s[e]=o}return s};export{d as deepMerge,l as isCatchAllOrOptional,T as isDynamic,A as isHttpMethod};
1
+ import{CATCH_ALL_PREFIX as c,DYNAMIC_PREFIX as a,HTTP_METHOD_FUNC_KEYS as p,OPTIONAL_CATCH_ALL_PREFIX as u}from"rpc4next-shared";const y=t=>t.startsWith(a),l=t=>t.startsWith(c)||t.startsWith(u),d=new Set(p),A=t=>d.has(t),i=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),h=(t,e)=>{const s={...t};for(const n in e)if(Object.hasOwn(e,n)){const r=t[n],o=e[n];i(r)&&i(o)?s[n]=h(r,o):s[n]=o}return s};export{h as deepMerge,l as isCatchAllOrOptional,y as isDynamic,A as isHttpMethod};
@@ -1,4 +1,4 @@
1
- import type { FuncParams, UrlOptions, ClientOptions, BodyOptions, HeadersOptions } from "./types";
1
+ import type { BodyOptions, ClientOptions, FuncParams, HeadersOptions, UrlOptions } from "./types";
2
2
  /** Local, non-breaking extension for future body shapes */
3
3
  type ExtendedBodyOptions = BodyOptions & {
4
4
  text?: string;
@@ -1 +1 @@
1
- import{deepMerge as x}from"./client-utils";import{createUrl as R}from"./url";import{normalizeHeaders as l}from"../lib/headers";const H=n=>{if(!n)return{};const{headers:i,headersInit:m,...p}=n;return p},U=n=>"content-type"in n?!0:Object.keys(n).some(i=>i.toLowerCase()==="content-type"),B=(n,i,m,p,g)=>async(d,O)=>{const a=n.replace(/^\$/,"").toUpperCase(),I=R([...i],m,p)(d?.url),C=O?.fetch??g.fetch??fetch,f=g.init,u=O?.init,w=l(f?.headers??f?.headersInit),T=l(u?.headers??u?.headersInit),j=l(d?.requestHeaders?.headers),o={...w,...T,...j},b=o.cookie,y=d?.requestHeaders?.cookies;if(y&&Object.keys(y).length>0){const s=Object.entries(y).map(([h,k])=>`${h}=${k}`).join("; ");o.cookie=b?`${b}; ${s}`:s}let t,r;const e=d?.body;e?.json!==void 0?(t=JSON.stringify(e.json),r="application/json"):typeof e?.text=="string"?(t=e.text,r="text/plain; charset=utf-8"):e?.formData instanceof FormData?(t=e.formData,r=void 0):e?.urlencoded instanceof URLSearchParams?(t=e.urlencoded,r="application/x-www-form-urlencoded; charset=UTF-8"):e?.raw!==void 0&&(t=e.raw,r=void 0),(a==="GET"||a==="HEAD")&&(t=void 0),!U(o)&&t&&r&&(o["content-type"]=r);const c=x(H(f),H(u));c.method=a,Object.keys(o).length>0&&(c.headers=o),t!==void 0&&(c.body=t);try{return await C(I.path,c)}catch(s){const h=s instanceof Error?s.message:String(s);throw new Error(`[httpMethod] ${a} ${I.path} failed: ${h}`,{cause:s})}};export{B as httpMethod};
1
+ import{normalizeHeaders as l}from"../lib/headers";import{deepMerge as x}from"./client-utils";import{createUrl as R}from"./url";const H=n=>{if(!n)return{};const{headers:i,headersInit:m,...p}=n;return p},U=n=>"content-type"in n?!0:Object.keys(n).some(i=>i.toLowerCase()==="content-type"),B=(n,i,m,p,g)=>async(d,O)=>{const a=n.replace(/^\$/,"").toUpperCase(),I=R([...i],m,p)(d?.url),C=O?.fetch??g.fetch??fetch,f=g.init,u=O?.init,w=l(f?.headers??f?.headersInit),T=l(u?.headers??u?.headersInit),j=l(d?.requestHeaders?.headers),o={...w,...T,...j},b=o.cookie,y=d?.requestHeaders?.cookies;if(y&&Object.keys(y).length>0){const s=Object.entries(y).map(([h,k])=>`${h}=${k}`).join("; ");o.cookie=b?`${b}; ${s}`:s}let t,r;const e=d?.body;e?.json!==void 0?(t=JSON.stringify(e.json),r="application/json"):typeof e?.text=="string"?(t=e.text,r="text/plain; charset=utf-8"):e?.formData instanceof FormData?(t=e.formData,r=void 0):e?.urlencoded instanceof URLSearchParams?(t=e.urlencoded,r="application/x-www-form-urlencoded; charset=UTF-8"):e?.raw!==void 0&&(t=e.raw,r=void 0),(a==="GET"||a==="HEAD")&&(t=void 0),!U(o)&&t&&r&&(o["content-type"]=r);const c=x(H(f),H(u));c.method=a,Object.keys(o).length>0&&(c.headers=o),t!==void 0&&(c.body=t);try{return await C(I.path,c)}catch(s){const h=s instanceof Error?s.message:String(s);throw new Error(`[httpMethod] ${a} ${I.path} failed: ${h}`,{cause:s})}};export{B as httpMethod};
@@ -1 +1 @@
1
- import{isCatchAllOrOptional as w}from"./client-utils";import{replaceDynamicSegments as x}from"./url";import{searchParamsToObject as L}from"../lib/search-params";const o=n=>{if(n!=null)try{return decodeURIComponent(n)}catch{return n}},$=n=>{const e=n.join("/").replace(/\/+/g,"/");return(e.startsWith("/")?e:`/${e}`).replace(/\/+$/,"")||"/"},O=(n,e)=>{const l=$(n),u=x(l,{optionalCatchAll:"(?:/(.*))?",catchAll:"/([^/]+(?:/[^/]+)*)",dynamic:"/([^/]+)"}),m=new RegExp(`^${u}(?:/)?$`);return p=>{let t;try{t=new URL(p,"http://dummy")}catch{return null}const f=t.pathname,d=m.exec(f);if(!d)return null;const a={};for(let s=0;s<e.length;s++){const h=e[s],i=h.replace(/^_+/,""),r=d[s+1];if(w(h))if(r==null||r==="")a[i]=void 0;else{const R=r.split("/").filter(Boolean).map(c=>o(c)).filter(c=>c!==void 0);a[i]=R}else a[i]=o(r)}const g=L(t.searchParams),P=t.hash?t.hash.slice(1):void 0,y=o(P);return{params:a,query:g,hash:y}}};export{O as matchPath};
1
+ import{searchParamsToObject as w}from"../lib/search-params";import{isCatchAllOrOptional as D}from"./client-utils";import{replaceDynamicSegments as x}from"./url";function o(n){if(n!=null)try{return decodeURIComponent(n)}catch{return n}}const L=n=>{const e=n.join("/").replace(/\/+/g,"/");return(e.startsWith("/")?e:`/${e}`).replace(/\/+$/,"")||"/"},C=(n,e)=>{const d=L(n),f=x(d,{optionalCatchAll:"(?:/(.*))?",catchAll:"/([^/]+(?:/[^/]+)*)",dynamic:"/([^/]+)"}),h=new RegExp(`^${f}(?:/)?$`);return g=>{let t;try{t=new URL(g,"http://dummy")}catch{return null}const m=t.pathname,u=h.exec(m);if(!u)return null;const i={};for(let a=0;a<e.length;a++){const l=e[a],s=l.replace(/^_+/,""),r=u[a+1];if(D(l))if(r==null||r==="")i[s]=void 0;else{const R=r.split("/").filter(Boolean).map(c=>o(c)).filter(c=>c!==void 0);i[s]=R}else i[s]=o(r)}const p=w(t.searchParams),P=t.hash?t.hash.slice(1):void 0,y=o(P);return{params:i,query:p,hash:y}}};export{C as matchPath};
@@ -1,10 +1,10 @@
1
+ import type { NextResponse } from "next/server";
2
+ import type { CATCH_ALL_PREFIX, DYNAMIC_PREFIX, HTTP_METHOD_FUNC_KEYS, OPTIONAL_CATCH_ALL_PREFIX } from "rpc4next-shared";
1
3
  import type { ContentType } from "../lib/content-type-types";
2
4
  import type { HttpRequestHeaders } from "../lib/http-request-headers-types";
3
5
  import type { HttpStatusCode } from "../lib/http-status-code-types";
4
6
  import type { RouteHandlerResponse, RouteResponse, ValidationSchema } from "../server/route-types";
5
7
  import type { TypedNextResponse, ValidationInputFor } from "../server/types";
6
- import type { NextResponse } from "next/server";
7
- import type { OPTIONAL_CATCH_ALL_PREFIX, CATCH_ALL_PREFIX, DYNAMIC_PREFIX, HTTP_METHOD_FUNC_KEYS } from "rpc4next-shared";
8
8
  type DistributeOmit<T, K extends keyof any> = T extends any ? Omit<T, K> : never;
9
9
  /**
10
10
  * Extension of the standard `RequestInit` interface with strongly typed headers.
@@ -1,4 +1,4 @@
1
- import type { UrlOptions, UrlResult, FuncParams } from "./types";
1
+ import type { FuncParams, UrlOptions, UrlResult } from "./types";
2
2
  /**
3
3
  * Builds a URL suffix string from optional query and hash values.
4
4
  *
@@ -1 +1 @@
1
- const m=t=>{if(!t)return"";const n=t.query?"?"+new URLSearchParams(t.query).toString():"",c=t.hash?`#${t.hash}`:"";return n+c},u=(t,n)=>t.replace(/\/_{5}(\w+)/g,n.optionalCatchAll).replace(/\/_{3}(\w+)/g,n.catchAll).replace(/\/_(\w+)/g,n.dynamic),$=(t,n,c)=>{const s=t.shift(),o=`/${t.join("/")}`,p=c.reduce((a,r)=>{const e=n[r];return Array.isArray(e)?a.replace(`/${r}`,`/${e.map(encodeURIComponent).join("/")}`):e===void 0?a.replace(`/${r}`,""):a.replace(`/${r}`,`/${encodeURIComponent(e)}`)},o);return a=>{const r=`${p}${m(a)}`,e=u(o,{optionalCatchAll:"/[[...$1]]",catchAll:"/[...$1]",dynamic:"/[$1]"}),i={};for(const l in n){const h=l.replace(/^_+/,"");i[h]=n[l]}return{pathname:e,params:i,path:s?`${s.replace(/\/$/,"")}${r}`:r,relativePath:r}}};export{m as buildUrlSuffix,$ as createUrl,u as replaceDynamicSegments};
1
+ const $=t=>{if(!t)return"";const n=t.query?`?${new URLSearchParams(t.query).toString()}`:"",c=t.hash?`#${t.hash}`:"";return n+c},m=(t,n)=>t.replace(/\/_{5}(\w+)/g,n.optionalCatchAll).replace(/\/_{3}(\w+)/g,n.catchAll).replace(/\/_(\w+)/g,n.dynamic),u=(t,n,c)=>{const s=t.shift(),o=`/${t.join("/")}`,p=c.reduce((a,r)=>{const e=n[r];return Array.isArray(e)?a.replace(`/${r}`,`/${e.map(encodeURIComponent).join("/")}`):e===void 0?a.replace(`/${r}`,""):a.replace(`/${r}`,`/${encodeURIComponent(e)}`)},o);return a=>{const r=`${p}${$(a)}`,e=m(o,{optionalCatchAll:"/[[...$1]]",catchAll:"/[...$1]",dynamic:"/[$1]"}),i={};for(const l in n){const h=l.replace(/^_+/,"");i[h]=n[l]}return{pathname:e,params:i,path:s?`${s.replace(/\/$/,"")}${r}`:r,relativePath:r}}};export{$ as buildUrlSuffix,u as createUrl,m as replaceDynamicSegments};
@@ -1,4 +1,4 @@
1
- import type { ValidationSchema, RouteResponse, Handler } from "./route-types";
2
- import type { Params, Query } from "./types";
3
1
  import type { HttpMethod } from "rpc4next-shared";
2
+ import type { Handler, RouteResponse, ValidationSchema } from "./route-types";
3
+ import type { Params, Query } from "./types";
4
4
  export declare const createHandler: <THttpMethod extends HttpMethod, TParams extends Params, TQuery extends Query, TValidationSchema extends ValidationSchema>() => <TRouteResponse extends RouteResponse>(handler: Handler<THttpMethod, TParams, TQuery, TValidationSchema, TRouteResponse>) => Handler<THttpMethod, TParams, TQuery, TValidationSchema, TRouteResponse>;
@@ -1,4 +1,4 @@
1
- export { routeHandlerFactory } from "./route-handler-factory";
2
- export type { TypedNextResponse } from "./types";
3
1
  export type { ContentType } from "../lib/content-type-types";
4
2
  export type { HttpStatusCode } from "../lib/http-status-code-types";
3
+ export { routeHandlerFactory } from "./route-handler-factory";
4
+ export type { TypedNextResponse } from "./types";
@@ -1,6 +1,6 @@
1
- import type { ValidationSchema } from "./route-types";
2
- import type { RouteContext, Query, Params } from "./types";
3
1
  import type { NextRequest } from "next/server";
2
+ import type { ValidationSchema } from "./route-types";
3
+ import type { Params, Query, RouteContext } from "./types";
4
4
  export declare const createRouteContext: <TParams extends Params, TQuery extends Query, TValidationSchema extends ValidationSchema>(req: NextRequest, segmentData: {
5
5
  params: Promise<TParams>;
6
6
  }) => RouteContext<TParams, TQuery, TValidationSchema>;
@@ -2,8 +2,8 @@
2
2
  * Inspired by Hono (https://github.com/honojs/hono),
3
3
  * particularly its routing design and handler interface.
4
4
  */
5
- import type { RequiredRouteResponse, ErrorHandler, RouteBindings, MethodRouteDefinition } from "./route-types";
6
- import type { Query, Params } from "./types";
5
+ import type { ErrorHandler, MethodRouteDefinition, RequiredRouteResponse, RouteBindings } from "./route-types";
6
+ import type { Params, Query } from "./types";
7
7
  /**
8
8
  * A factory function that creates route handlers for various HTTP methods (GET, POST, etc.).
9
9
  *
@@ -6,10 +6,10 @@
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
8
  */
9
- import type { TypedNextResponse, Query, RouteContext, Params } from "./types";
10
9
  import type { NextRequest } from "next/server";
11
10
  import type { HttpMethod } from "rpc4next-shared";
12
- export type RouteResponse = TypedNextResponse | Promise<TypedNextResponse | void>;
11
+ import type { Params, Query, RouteContext, TypedNextResponse } from "./types";
12
+ export type RouteResponse = TypedNextResponse | Promise<TypedNextResponse | undefined>;
13
13
  export type RequiredRouteResponse = TypedNextResponse | Promise<TypedNextResponse>;
14
14
  export interface RouteBindings {
15
15
  params?: Params | Promise<Params>;
@@ -21,7 +21,7 @@ export interface ValidationSchema {
21
21
  }
22
22
  export type Handler<_THttpMethod extends HttpMethod, TParams = Params, TQuery = Query, TValidationSchema extends ValidationSchema = ValidationSchema, TRouteResponse extends RouteResponse = RouteResponse> = (routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => TRouteResponse;
23
23
  export type ErrorHandler<TRouteResponse extends RequiredRouteResponse, TParams = Params, TQuery = Query, TValidationSchema extends ValidationSchema = ValidationSchema> = (error: unknown, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => TRouteResponse;
24
- export type RouteHandlerResponse<TRouteResponse extends RouteResponse, _TValidationSchema extends ValidationSchema> = Promise<Exclude<Awaited<TRouteResponse>, void | undefined>>;
24
+ export type RouteHandlerResponse<TRouteResponse extends RouteResponse, _TValidationSchema extends ValidationSchema> = Promise<Exclude<Awaited<TRouteResponse>, undefined | undefined>>;
25
25
  export type RouteHandler<TParams extends RouteBindings["params"], TRouteResponse extends RouteResponse, TValidationSchema extends ValidationSchema> = (req: NextRequest, segmentData: {
26
26
  params: Promise<TParams>;
27
27
  }) => RouteHandlerResponse<TRouteResponse, TValidationSchema>;
@@ -1,9 +1,9 @@
1
- import type { ValidationSchema } from "./route-types";
1
+ import type { NextRequest, NextResponse } from "next/server";
2
+ import type { HttpMethod } from "rpc4next-shared";
2
3
  import type { ContentType } from "../lib/content-type-types";
3
4
  import type { HttpResponseHeaders } from "../lib/http-response-headers-types";
4
5
  import type { HttpStatusCode, RedirectionHttpStatusCode, SuccessfulHttpStatusCode } from "../lib/http-status-code-types";
5
- import type { NextResponse, NextRequest } from "next/server";
6
- import type { HttpMethod } from "rpc4next-shared";
6
+ import type { ValidationSchema } from "./route-types";
7
7
  /**
8
8
  * Represents the result of an HTTP response status check.
9
9
  *
@@ -6,7 +6,7 @@
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
8
  */
9
+ import type { HttpMethod } from "rpc4next-shared";
9
10
  import type { ValidationSchema } from "../route-types";
10
11
  import type { Params, Query, RouteContext, TypedNextResponse, ValidatedData, ValidationTarget } from "../types";
11
- import type { HttpMethod } from "rpc4next-shared";
12
12
  export declare const validator: <THttpMethod extends HttpMethod, TValidationTarget extends ValidationTarget<THttpMethod>, TParams extends Params, TQuery extends Query, TValidationSchema extends ValidationSchema>() => <TTypedNextResponse extends TypedNextResponse>(target: TValidationTarget, validateHandler: (value: object, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => Promise<ValidatedData | TTypedNextResponse>) => import("../route-types").Handler<THttpMethod, TParams, TQuery, TValidationSchema, Promise<TTypedNextResponse | undefined>>;
@@ -6,11 +6,11 @@
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
8
  */
9
- import type { ValidationSchema } from "../../route-types";
10
- import type { RouteContext, Params, Query, TypedNextResponse, ConditionalValidationInput, ValidationTarget } from "../../types";
11
9
  import type { HttpMethod } from "rpc4next-shared";
12
- import type { z, ZodSchema } from "zod";
10
+ import type { ZodSchema, z } from "zod";
11
+ import type { ValidationSchema } from "../../route-types";
12
+ import type { ConditionalValidationInput, Params, Query, RouteContext, TypedNextResponse, ValidationTarget } from "../../types";
13
13
  export declare const zValidator: <THttpMethod extends HttpMethod, TValidationTarget extends ValidationTarget<THttpMethod>, TSchema extends ZodSchema<any>, TParams extends ConditionalValidationInput<TValidationTarget, "params", TValidationSchema, Params> & Params, TQuery extends ConditionalValidationInput<TValidationTarget, "query", TValidationSchema, Query> & Query, TInput = z.input<TSchema>, TOutput = z.output<TSchema>, TValidationSchema extends ValidationSchema = {
14
14
  input: Record<TValidationTarget, TInput>;
15
15
  output: Record<TValidationTarget, TOutput>;
16
- }, THookReturn extends TypedNextResponse | void = TypedNextResponse<z.ZodSafeParseError<TInput>, 400, "application/json"> | void>(target: TValidationTarget, schema: TSchema, hook?: (result: z.ZodSafeParseResult<TOutput>, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => THookReturn) => import("../../route-types").Handler<THttpMethod, TParams, TQuery, TValidationSchema, Promise<Exclude<THookReturn, void> | undefined>>;
16
+ }, THookReturn extends TypedNextResponse | undefined = TypedNextResponse<z.ZodSafeParseError<TInput>, 400, "application/json"> | undefined>(target: TValidationTarget, schema: TSchema, hook?: (result: z.ZodSafeParseResult<TOutput>, routeContext: RouteContext<TParams, TQuery, TValidationSchema>) => THookReturn) => import("../../route-types").Handler<THttpMethod, TParams, TQuery, TValidationSchema, Promise<Exclude<THookReturn, void> | undefined>>;
@@ -5,4 +5,4 @@
5
5
  * This code has been adapted and modified for this project.
6
6
  * Original copyright belongs to Yusuke Wada and the Hono project contributors.
7
7
  * Hono is licensed under the MIT License.
8
- */import{validator as s}from"../validator";const T=(n,i,r)=>{const d=r??((t,a)=>{if(!t.success)return a.json(t,{status:400})});return s()(n,async(t,a)=>{const e=await i.safeParseAsync(t),o=d(e,a);if(o instanceof Response)return o;if(!e.success)throw new Error("If you provide a custom hook, you must explicitly return a response when validation fails.");return e.data})};export{T as zValidator};
8
+ */import{validator as s}from"../validator";const T=(n,i,r)=>{const d=r??((t,e)=>{if(!t.success)return e.json(t,{status:400})});return s()(n,async(t,e)=>{const a=await i.safeParseAsync(t),o=d(a,e);if(o instanceof Response)return o;if(!a.success)throw new Error("If you provide a custom hook, you must explicitly return a response when validation fails.");return a.data})};export{T as zValidator};
package/package.json CHANGED
@@ -1,23 +1,20 @@
1
1
  {
2
2
  "name": "rpc4next",
3
- "version": "0.4.14",
3
+ "version": "0.5.0",
4
4
  "description": "Inspired by Hono RPC and Pathpida, rpc4next brings a lightweight and intuitive RPC solution to Next.js, making server-client communication seamless",
5
- "author": "watanabe-1",
6
- "license": "MIT",
5
+ "keywords": [
6
+ "next.js",
7
+ "rpc",
8
+ "typescript"
9
+ ],
7
10
  "homepage": "https://github.com/watanabe-1/rpc4next#readme",
8
11
  "repository": {
9
12
  "type": "git",
10
13
  "url": "git+https://github.com/watanabe-1/rpc4next.git",
11
14
  "directory": "packages/rpc4next"
12
15
  },
13
- "publishConfig": {
14
- "access": "public"
15
- },
16
- "keywords": [
17
- "next.js",
18
- "rpc",
19
- "typescript"
20
- ],
16
+ "license": "MIT",
17
+ "author": "watanabe-1",
21
18
  "type": "module",
22
19
  "module": "dist/index.js",
23
20
  "types": "dist/index.d.ts",
@@ -94,14 +91,17 @@
94
91
  "test:coverage": "vitest run --coverage.enabled true",
95
92
  "test:ui": "vitest --ui --coverage.enabled true",
96
93
  "test:watch": "vitest --watch",
97
- "lint": "eslint src",
98
- "lint:fix": "eslint src --fix",
94
+ "lint": "biome check src",
95
+ "lint:fix": "biome check --write src",
99
96
  "typecheck": "tsc -b tsconfig.build.json --noEmit"
100
97
  },
101
98
  "dependencies": {
102
- "rpc4next-shared": "^0.1.5"
99
+ "rpc4next-shared": "^0.2.0"
103
100
  },
104
101
  "peerDependencies": {
105
102
  "next": "^14.0.0 || ^15.0.0"
103
+ },
104
+ "publishConfig": {
105
+ "access": "public"
106
106
  }
107
107
  }