rpc4next 0.4.14 → 0.5.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/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,377 @@
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
+ ## Development Notes
12
+
13
+ This repository is a monorepo.
14
+
15
+ - `packages/rpc4next` contains the runtime client/server helpers.
16
+ - `packages/rpc4next-cli` contains the Next.js scanner and type generator.
17
+ - `integration/next-app` is the real end-to-end fixture app used to verify generated artifacts, runtime behavior, and browser usage together.
18
+
19
+ When a change affects scanner behavior, generated path structure output, params generation, or integration fixture routes, regenerate the integration artifacts and review those diffs as part of the change.
20
+
21
+ ---
22
+
23
+ ## ✨ Features
24
+
25
+ - ✅ ルート、パラメータ、クエリパラメータ、 リクエストボディ、レスポンスの型安全なクライアント生成
26
+ - ✅ 既存の `app/**/route.ts` および `app/**/page.tsx` を活用するため、新たなハンドラファイルの作成は不要
27
+ - ✅ 最小限のセットアップで、カスタムサーバー不要
28
+ - ✅ 動的ルート(`[id]`、`[...slug]` など)に対応
29
+ - ✅ CLI による自動クライアント用型定義生成
30
+
31
+ ---
32
+
33
+ ## 🚀 Getting Started
34
+
35
+ ### 1. Install Packages
36
+
37
+ ```bash
38
+ npm install rpc4next
39
+ npm install -D rpc4next-cli
40
+ ```
41
+
42
+ ### 2. Define API Routes in Next.js
43
+
44
+ Next.js プロジェクト内の既存の `app/**/route.ts` と `app/**/page.tsx` ファイルをそのまま利用できます。
45
+ さらに、クエリパラメータ(searchParams)の型安全性を有効にするには、対象のファイル内で `Query` 型を定義し、`export` してください。
46
+
47
+ ```ts
48
+ // app/api/user/[id]/route.ts
49
+ import { NextRequest, NextResponse } from "next/server";
50
+
51
+ // searchParams用の型定義
52
+ export type Query = {
53
+ q: string; // 必須
54
+ page?: number; // 任意
55
+ };
56
+
57
+ export async function GET(
58
+ req: NextRequest,
59
+ segmentData: { params: Promise<{ id: string }> },
60
+ ) {
61
+ const { id } = await segmentData.params;
62
+ const q = req.nextUrl.searchParams.get("q");
63
+ return NextResponse.json({ id, q });
64
+ }
65
+ ```
66
+
67
+ 🚩 Query 型を export することで、searchParams の型も自動的にクライアントに反映されます。
68
+
69
+ - **RPCとしてresponseの戻り値の推論が機能するのは、対象となる `route.ts` の HTTPメソッドハンドラ内で`NextResponse.json()` をしている関数のみになります**
70
+
71
+ ---
72
+
73
+ ### 3. Generate Type Definitions with CLI
74
+
75
+ CLI を利用して、Next.js のルート構造から型安全な RPC クライアントの定義を自動生成します。
76
+
77
+ ```bash
78
+ npx rpc4next <baseDir> <outputPath>
79
+ ```
80
+
81
+ `rpc4next` command is provided by the `rpc4next-cli` package.
82
+
83
+ ```json
84
+ {
85
+ "baseDir": "app",
86
+ "outputPath": "src/generated/rpc.ts",
87
+ "paramsFile": "params.ts"
88
+ }
89
+ ```
90
+
91
+ `rpc4next.config.json` を実行ディレクトリに置くと、固定値を CLI 引数に繰り返し書かずに済みます。
92
+
93
+ ```bash
94
+ npx rpc4next
95
+ npx rpc4next --watch
96
+ ```
97
+
98
+ - `<baseDir>`: Next.js の Appルータが配置されたベースディレクトリ
99
+ - `<outputPath>`: 生成された型定義ファイルの出力先
100
+
101
+ #### オプション
102
+
103
+ - **ウォッチモード**
104
+ ファイル変更を検知して自動的に再生成する場合は `--watch` or `-w` オプションを付けます。
105
+
106
+ ```bash
107
+ npx rpc4next <baseDir> <outputPath> --watch
108
+ ```
109
+
110
+ - **パラメータ型ファイルの生成**
111
+ 各ルートに対して個別のパラメータ型定義ファイルを生成する場合は、`--params-file` or `-p` オプションにファイル名を指定します。
112
+
113
+ ```bash
114
+ npx rpc4next <baseDir> <outputPath> --params-file <paramsFileName>
115
+ ```
116
+
117
+ ---
118
+
119
+ ### 4. Create Your RPC Client
120
+
121
+ 生成された型定義ファイルを基に、RPC クライアントを作成します。
122
+
123
+ ```ts
124
+ // lib/rpcClient.ts
125
+ import { createClient } from "rpc4next/client";
126
+ import type { PathStructure } from "あなたが生成した型定義ファイル";
127
+
128
+ export const rpc = createClient<PathStructure>();
129
+ ```
130
+
131
+ ---
132
+
133
+ ### 5. Use It in Your Components
134
+
135
+ コンポーネント内で生成された RPC クライアントを使用します。
136
+
137
+ ```tsx
138
+ // app/page.tsx
139
+ import { rpc } from "@/lib/rpcClient";
140
+
141
+ export default async function Page() {
142
+ const res = await rpc.api.user._id("123").$get({
143
+ query: { q: "hello", page: 1 },
144
+ });
145
+ const json = await res.json();
146
+ return <div>{json.q}</div>;
147
+ }
148
+ ```
149
+
150
+ - エディタの補完機能により、利用可能なエンドポイントが自動的に表示されます。
151
+ - リクエストの構造(params, query)はサーバーコードから推論され、レスポンスも型安全に扱えます。
152
+
153
+ ---
154
+
155
+ ## ✅ さらに型安全にしたい場合 `createRouteHandler` による Next.js の型安全強化
156
+
157
+ ### 📌 主なメリット
158
+
159
+ 1. **レスポンス型安全**
160
+ - ステータス、Content-Type、Body がすべて型で保証される
161
+ - クライアントは受け取るレスポンス型を完全に推論可能
162
+
163
+ 2. **クライアント側補完強化**
164
+ - `status`, `content-type`, `json()`, `text()` などが適切に補完される
165
+
166
+ 3. **サーバー側 params / query も型安全**
167
+ - `createRouteHandler()` + `zValidator()` を使えば、`params`, `query`, `headers`, `cookies`, `json` も型推論・バリデーション可能
168
+ - `createRouteHandler()` + `zValidator()` を使えば、`Query` 型もexportする必要なし
169
+
170
+ ---
171
+
172
+ ### ✅ 基本的な使い方
173
+
174
+ ```ts
175
+ const createRouteHandler = routeHandlerFactory((err, rc) =>
176
+ rc.text("error", { status: 400 }),
177
+ );
178
+
179
+ const { POST } = createRouteHandler().post(async (rc) => rc.text("plain text"));
180
+ ```
181
+
182
+ これだけで、POSTリクエストの返り値に、レスポンスの内容 (`json`, `text`など)、`status`, `content-type` が型付けされるようになります。
183
+
184
+ ---
185
+
186
+ ### 👤 サーバー側でのより型安全なルート作成
187
+
188
+ `createRouteHandler()` と `zValidator()` を使うことで、各リクエストパーツに対して **型安全なバリデーション** をかけられます。
189
+
190
+ #### シンプルな例
191
+
192
+ ```ts
193
+ import { createRouteHandler } from "@/path/to/createRouteHandler";
194
+ import { zValidator } from "@/path/to/zValidator";
195
+ import { z } from "zod";
196
+
197
+ // Zodスキーマを定義
198
+ const paramsSchema = z.object({
199
+ userId: z.string(),
200
+ });
201
+
202
+ // バリデーション付きルートハンドラを作成
203
+ export const { GET } = createRouteHandler<{
204
+ params: z.infer<typeof paramsSchema>;
205
+ }>().get(
206
+ zValidator("params", paramsSchema), // paramsを検証
207
+ async (rc) => {
208
+ const params = rc.req.valid("params"); // バリデーション済みparamsを取得
209
+ return rc.json({ message: `User ID is ${params.userId}` });
210
+ },
211
+ );
212
+ ```
213
+
214
+ ## ✅ サポートされているバリデーションターゲット
215
+
216
+ サーバー側では,次のリクエスト部分を型安全に検証できます:
217
+
218
+ | ターゲット | 説明 |
219
+ | :--------- | :-------------------------------------------------- |
220
+ | `params` | URLパラメータ ( `/user/:id` の `id`など) |
221
+ | `query` | クエリパラメータ (`?q=xxx&page=1`など) |
222
+ | `headers` | リクエストヘッダー |
223
+ | `cookies` | クッキー |
224
+ | `json` | リクエストボディ (Content-Type: `application/json`) |
225
+
226
+ ---
227
+
228
+ ### 🔥 複数ターゲットを同時に検証する例
229
+
230
+ ```ts
231
+ import { createRouteHandler } from "@/path/to/createRouteHandler";
232
+ import { zValidator } from "@/path/to/zValidator";
233
+ import { z } from "zod";
234
+
235
+ const querySchema = z.object({
236
+ page: z.string().regex(/^\d+$/),
237
+ });
238
+
239
+ const jsonSchema = z.object({
240
+ name: z.string(),
241
+ age: z.number(),
242
+ });
243
+
244
+ export const { POST } = createRouteHandler<{
245
+ query: z.infer<typeof querySchema>;
246
+ }>().post(
247
+ zValidator("query", querySchema),
248
+ zValidator("json", jsonSchema),
249
+ async (rc) => {
250
+ const query = rc.req.valid("query");
251
+ const body = rc.req.valid("json");
252
+ return rc.json({ query, body });
253
+ },
254
+ );
255
+ ```
256
+
257
+ - `query`と`json`を別々のスキーマで検証
258
+ - **成功時は型安全に取得可能** (`rc.req.valid('query')`, `rc.req.valid('json')`)
259
+
260
+ ---
261
+
262
+ これにより,クライアント側とサーバー側が、全面的に**型でつながる**ので,ミスを何次も防げ,開発体験を大幅に向上できます。
263
+
264
+ ---
265
+
266
+ ### ⚡ バリデーション失敗時のカスタムエラーハンドリング
267
+
268
+ - デフォルトでは、バリデーション失敗時に自動で `400 Bad Request` を返します。
269
+ - 必要に応じて、**カスタムフック**でエラー対応を制御できます。
270
+
271
+ ```ts
272
+ zValidator("params", paramsSchema, (result, rc) => {
273
+ if (!result.success) {
274
+ return rc.json({ error: result.error.errors }, { status: 422 });
275
+ }
276
+ });
277
+ ```
278
+
279
+ > (フック内でレスポンスを返さない場合は、通常通り例外がスローされます)
280
+
281
+ ---
282
+
283
+ ## 📡 クライアント側での使い方
284
+
285
+ `rpc4next`で作成したクライアントは、`createRouteHandler` と `zValidator` で作成したルートハンドラの内容にしたがって **params, query, headers, cookies, json** を型安全に送信できます。
286
+
287
+ 例:
288
+
289
+ ```ts
290
+ import { createRpcClient } from "@/path/to/rpc-client";
291
+ import type { PathStructure } from "@/path/to/generated-types";
292
+
293
+ const client = createRpcClient<PathStructure>("http://localhost:3000");
294
+
295
+ async function callUserApi() {
296
+ const res = await client.api.menu.test.$post({
297
+ body: { json: { age: 20, name: "foo" } },
298
+ url: { query: { page: "1" } },
299
+ });
300
+
301
+ if (res.ok) {
302
+ const json = await res.json();
303
+
304
+ // ✅ 正常時は次の型が推論されます
305
+ // const json: {
306
+ // query: {
307
+ // page: string;
308
+ // };
309
+ // body: {
310
+ // name: string;
311
+ // age: number;
312
+ // };
313
+ // }
314
+ } else {
315
+ const error = await res.json();
316
+
317
+ // ⚠️ バリデーションエラー時は次の型が推論されます
318
+ // const error:
319
+ // | SafeParseError<{
320
+ // page: string;
321
+ // }>
322
+ // | SafeParseError<{
323
+ // name: string;
324
+ // age: number;
325
+ // }>;
326
+ }
327
+ }
328
+ ```
329
+
330
+ - エディタの補完機能により、送信できるターゲットが明示されます
331
+ - サーバー側の型定義に基づいて、**型のズレを防止**できます
332
+
333
+ ---
334
+
335
+ これらのように、リクエスト時にはさまざまなターゲット (`params`, `query`, `headers`, `cookies`, `json`) を送信できます。
336
+
337
+ さらに、サーバー側では、これらを**個別に型付け、バリデーション**できます。
338
+
339
+ ---
340
+
341
+ ## 🧭 Monorepo Layout
342
+
343
+ - `packages/rpc4next`: Core library modules (client, server, validators, shared types)
344
+ - `packages/rpc4next-cli`: CLI generator that exposes the `rpc4next` binary
345
+ - Install once at the repo root: `bun install`
346
+ - Build everything: `bun run build`
347
+ - Run all tests: `bun run test`
348
+ - Lint all packages: `bun run lint`
349
+
350
+ ## 🚧 Requirements
351
+
352
+ - Next.js 14+ (App Router 使用)
353
+ - Node.js 20.19.2+
354
+ - aqua (Node.js / Bun のバージョン管理)
355
+
356
+ ```bash
357
+ aqua i
358
+ ```
359
+
360
+ - `aqua/aqua.yaml` を更新したら、チェックサムを更新してください
361
+
362
+ ```bash
363
+ aqua update-checksum
364
+ ```
365
+
366
+ - CI / ローカルで設定を強制する場合は以下を利用してください
367
+
368
+ ```bash
369
+ export AQUA_ENFORCE_CHECKSUM=true
370
+ export AQUA_ENFORCE_REQUIRE_CHECKSUM=true
371
+ ```
372
+
373
+ ---
374
+
375
+ ## 💼 License
376
+
377
+ 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";
@@ -7,5 +7,9 @@ export declare const isDynamic: (key: string) => boolean;
7
7
  * Returns true if the key represents a catch-all or optional catch-all segment.
8
8
  */
9
9
  export declare const isCatchAllOrOptional: (key: string) => boolean;
10
+ /**
11
+ * Returns true if the key represents an optional catch-all segment.
12
+ */
13
+ export declare const isOptionalCatchAll: (key: string) => boolean;
10
14
  export declare const isHttpMethod: (value: string) => value is HttpMethodFuncKey;
11
- export declare const deepMerge: <T extends object, U extends object>(target: T, source: U) => T;
15
+ export declare const deepMerge: <T extends object, U extends object>(target: T, source: U) => T & U;
@@ -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 a,DYNAMIC_PREFIX as p,HTTP_METHOD_FUNC_KEYS as h,OPTIONAL_CATCH_ALL_PREFIX as i}from"rpc4next-shared";const g=t=>t.startsWith(p),y=t=>t.startsWith(a)||t.startsWith(i),A=t=>t.startsWith(i),u=new Set(h),T=t=>u.has(t),c=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),d=(t,s)=>{const e={...t};for(const n in s)if(Object.hasOwn(s,n)){const r=t[n],o=s[n];c(r)&&c(o)?e[n]=d(r,o):e[n]=o}return e};export{d as deepMerge,y as isCatchAllOrOptional,g as isDynamic,T as isHttpMethod,A as isOptionalCatchAll};
@@ -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 P}from"../lib/search-params";import{isCatchAllOrOptional as x}from"./client-utils";function a(n){if(n!=null)try{return decodeURIComponent(n)}catch{return n}}const D=n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),O=(n,u)=>{const c=n.map(t=>t.replace(/^\/+|\/+$/g,"")).filter(Boolean),h=c.length===0?"/":c.reduce((t,e)=>u.includes(e)?e.startsWith("_____")?`${t}(?:/(.*))?`:e.startsWith("___")?`${t}/([^/]+(?:/[^/]+)*)`:`${t}/([^/]+)`:`${t}/${D(a(e))}`,""),g=new RegExp(`^${h}(?:/)?$`);return t=>{let e;try{e=new URL(t,"http://dummy")}catch{return null}const p=e.pathname,l=g.exec(p);if(!l)return null;const i={};for(let s=0;s<u.length;s++){const f=u[s],o=f.replace(/^_+/,""),r=l[s+1];if(x(f))if(r==null||r==="")i[o]=void 0;else{const R=r.split("/").filter(Boolean).map(d=>a(d)).filter(d=>d!==void 0);i[o]=R}else i[o]=a(r)}const m=P(e.searchParams),$=e.hash?e.hash.slice(1):void 0,_=a($);return{params:i,query:m,hash:_}}};export{O as matchPath};
@@ -2,4 +2,4 @@
2
2
  * Inspired by the design of Hono (https://github.com/honojs/hono)
3
3
  * and pathpida (https://github.com/aspida/pathpida)
4
4
  * particularly their routing structures and developer experience.
5
- */import{isDynamic as g}from"./client-utils";const l=(r,e,n,s,o)=>{const p=function(){};return new Proxy(p,{apply(a,u,t){const i=t[0],c=n[n.length-1],f=o[o.length-1];if(!c||!g(c))throw new Error(`Cannot apply a value: "${c??""}" is not a dynamic segment.`);if(i===void 0){const d=f??c;throw new Error(`Missing value for dynamic parameter: ${String(d)}`)}return l(r,e,n,{...s,[f]:i},o)},get(a,u){if(u==="then"||typeof u=="symbol")return;const t=String(u),i=r(t,{paths:n,params:s,dynamicKeys:o,options:e});return i!==void 0?i:g(t)?l(r,e,[...n,t],s,[...o,t]):l(r,e,[...n,t],s,o)}})},T=r=>(e="/",n={})=>l(r,n,[e],{},[]);export{T as makeCreateRpc};
5
+ */import{isDynamic as g,isOptionalCatchAll as w}from"./client-utils";const a=(r,e,n,l,o)=>{const p=function(){};return new Proxy(p,{apply(c,u,t){const i=t[0],s=n[n.length-1],f=o[o.length-1];if(!s||!g(s))throw new Error(`Cannot apply a value: "${s??""}" is not a dynamic segment.`);if(i===void 0&&!w(s)){const d=f??s;throw new Error(`Missing value for dynamic parameter: ${String(d)}`)}return a(r,e,n,{...l,[f]:i},o)},get(c,u){if(u==="then"||typeof u=="symbol")return;const t=String(u),i=r(t,{paths:n,params:l,dynamicKeys:o,options:e});return i!==void 0?i:g(t)?a(r,e,[...n,t],l,[...o,t]):a(r,e,[...n,t],l,o)}})},x=r=>(e="/",n={})=>a(r,n,[e],{},[]);export{x as makeCreateRpc};
@@ -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 r=t.query?`?${new URLSearchParams(t.query).toString()}`:"",c=t.hash?`#${t.hash}`:"";return r+c},f=(t,r)=>t.replace(/\/_{5}(\w+)/g,r.optionalCatchAll).replace(/\/_{3}(\w+)/g,r.catchAll).replace(/\/_(\w+)/g,r.dynamic),l=t=>{try{return decodeURIComponent(t)}catch{return t}},p=t=>{const r=t.join("/");return(r?`/${r}`:"/").replace(/\/+/g,"/").replace(/\/+$/,"")||"/"},g=t=>t.startsWith("_____")?`[[...${t.slice(5)}]]`:t.startsWith("___")?`[...${t.slice(3)}]`:t.startsWith("_")?`[${t.slice(1)}]`:l(t),m=(t,r,c)=>{const s=t.shift(),h=p(t.map(n=>c.includes(n)?n:l(n))),u=c.reduce((n,e)=>{const a=r[e];return Array.isArray(a)?n.replace(`/${e}`,`/${a.map(encodeURIComponent).join("/")}`):a===void 0?n.replace(`/${e}`,""):n.replace(`/${e}`,`/${encodeURIComponent(a)}`)},h);return n=>{const e=`${u}${$(n)}`,a=p(t.map(g)),i={};for(const o in r){const d=o.replace(/^_+/,"");i[d]=r[o]}return{pathname:a,params:i,path:s?`${s.replace(/\/$/,"")}${e}`:e,relativePath:e}}};export{$ as buildUrlSuffix,m as createUrl,f 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,24 +1,24 @@
1
1
  {
2
2
  "name": "rpc4next",
3
- "version": "0.4.14",
3
+ "version": "0.5.1",
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",
19
+ "engines": {
20
+ "node": ">=20.19.2"
21
+ },
22
22
  "module": "dist/index.js",
23
23
  "types": "dist/index.d.ts",
24
24
  "files": [
@@ -94,14 +94,17 @@
94
94
  "test:coverage": "vitest run --coverage.enabled true",
95
95
  "test:ui": "vitest --ui --coverage.enabled true",
96
96
  "test:watch": "vitest --watch",
97
- "lint": "eslint src",
98
- "lint:fix": "eslint src --fix",
99
- "typecheck": "tsc -b tsconfig.build.json --noEmit"
97
+ "lint": "biome check src",
98
+ "lint:fix": "biome check --write src",
99
+ "typecheck": "tsc -p tsconfig.build.json --noEmit"
100
100
  },
101
101
  "dependencies": {
102
- "rpc4next-shared": "^0.1.5"
102
+ "rpc4next-shared": "^0.3.0"
103
103
  },
104
104
  "peerDependencies": {
105
105
  "next": "^14.0.0 || ^15.0.0"
106
+ },
107
+ "publishConfig": {
108
+ "access": "public"
106
109
  }
107
110
  }