rpc4next 0.5.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
@@ -2,331 +2,413 @@
2
2
 
3
3
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/watanabe-1/rpc4next)
4
4
 
5
- Lightweight, type-safe RPC system for Next.js App Router projects.
5
+ `rpc4next` is a lightweight, type-safe RPC layer for Next.js App Router projects.
6
+ It scans your existing `app/**` files, generates a `PathStructure` type, and lets you call route handlers through a typed client without introducing a custom server framework.
6
7
 
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
+ It is inspired by Hono RPC and Pathpida:
8
9
 
9
- ---
10
+ - `route.ts` files become typed RPC endpoints
11
+ - `page.tsx` files become typed URL/path entries
12
+ - dynamic segments and exported route `Query` types are reflected in generated client types
13
+ - optional generated `params.ts` files can give route files a stable sibling `Params` type
10
14
 
11
- ## Features
15
+ If you want to see a full working example, start with the real integration fixture in [integration/next-app/README.md](./integration/next-app/README.md). It shows how route scanning, generated types, the client, and a real Next.js app fit together in this repository.
12
16
 
13
- - ルート、パラメータ、クエリパラメータ、 リクエストボディ、レスポンスの型安全なクライアント生成
14
- - ✅ 既存の `app/**/route.ts` および `app/**/page.tsx` を活用するため、新たなハンドラファイルの作成は不要
15
- - ✅ 最小限のセットアップで、カスタムサーバー不要
16
- - ✅ 動的ルート(`[id]`、`[...slug]` など)に対応
17
- - ✅ CLI による自動クライアント用型定義生成
17
+ ## What It Covers
18
18
 
19
- ---
19
+ - Typed client calls for `app/**/route.ts`
20
+ - Typed URL generation for `app/**/page.tsx`
21
+ - Dynamic routes, catch-all routes, and optional catch-all routes
22
+ - Route groups and parallel-route descendants
23
+ - Validation helpers for `params`, `query`, `json`, `headers`, and `cookies`
24
+ - Plain Next.js route handlers written with `NextResponse.json(...)` or `Response.json(...)`
20
25
 
21
- ## 🚀 Getting Started
26
+ Routing notes:
22
27
 
23
- ### 1. Install Packages
28
+ - Route group folders do not appear in generated public paths
29
+ - Parallel route slot names are excluded, but their descendant pages are flattened onto public URL paths
30
+ - Intercepting route branches are excluded from `PathStructure` because rpc4next models public URL paths
31
+
32
+ This is a good fit if you want typed client calls and typed URLs from an existing App Router codebase without moving to a custom RPC server framework. If you already want to keep writing normal `route.ts` and `page.tsx` files, `rpc4next` is designed for that.
33
+
34
+ ## Requirements
35
+
36
+ - Node.js `>=20.19.2`
37
+ - Next.js App Router
38
+ - Package peer dependency support in `rpc4next` and `rpc4next-cli`: Next.js `^14`, `^15`, or `^16`
39
+
40
+ ## Installation
24
41
 
25
42
  ```bash
26
43
  npm install rpc4next
27
44
  npm install -D rpc4next-cli
28
45
  ```
29
46
 
30
- ### 2. Define API Routes in Next.js
47
+ If you use Bun in your project:
48
+
49
+ ```bash
50
+ bun add rpc4next
51
+ bun add -d rpc4next-cli
52
+ ```
53
+
54
+ `zod` is only needed if you use the server-side validation helpers such as
55
+ `zValidator()`. If you only use the generated client types and do not validate
56
+ request input with Zod, you can omit it.
57
+
58
+ If you want Zod-based request validation later:
59
+
60
+ ```bash
61
+ npm install zod
62
+ ```
63
+
64
+ ## Quick Start
65
+
66
+ If you prefer to inspect a complete app before wiring this into your own project, see [integration/next-app/README.md](./integration/next-app/README.md).
31
67
 
32
- Next.js プロジェクト内の既存の `app/**/route.ts` `app/**/page.tsx` ファイルをそのまま利用できます。
33
- さらに、クエリパラメータ(searchParams)の型安全性を有効にするには、対象のファイル内で `Query` 型を定義し、`export` してください。
68
+ ### 1. Define a Route
69
+
70
+ `rpc4next` does not require `routeHandlerFactory()`.
71
+ It can scan and generate client types from standard Next.js App Router handlers as-is.
72
+ The server helpers are optional and mainly give you stronger response and validation typing.
73
+
74
+ If you want the stronger typed server-side experience, use `routeHandlerFactory()`:
34
75
 
35
76
  ```ts
36
- // app/api/user/[id]/route.ts
37
- import { NextRequest, NextResponse } from "next/server";
77
+ // app/api/users/[userId]/route.ts
78
+ import { routeHandlerFactory } from "rpc4next/server";
38
79
 
39
- // searchParams用の型定義
40
80
  export type Query = {
41
- q: string; // 必須
42
- page?: number; // 任意
81
+ includePosts?: "true" | "false";
43
82
  };
44
83
 
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
- }
84
+ const createRouteHandler = routeHandlerFactory();
85
+
86
+ export const { GET } = createRouteHandler<{
87
+ params: { userId: string };
88
+ query: Query;
89
+ }>().get(async (rc) => {
90
+ const { userId } = await rc.req.params();
91
+ const query = rc.req.query();
92
+
93
+ return rc.json({
94
+ ok: true,
95
+ userId,
96
+ includePosts: query.includePosts === "true",
97
+ });
98
+ });
53
99
  ```
54
100
 
55
- 🚩 Query 型を export することで、searchParams の型も自動的にクライアントに反映されます。
101
+ Notes:
56
102
 
57
- - **RPCとしてresponseの戻り値の推論が機能するのは、対象となる `route.ts` の HTTPメソッドハンドラ内で`NextResponse.json()` をしている関数のみになります**
103
+ - `routeHandlerFactory()` is optional, not required
104
+ - Export `Query` from a route if you want the generated client to type `searchParams` for plain Next.js handlers too
105
+ - `routeHandlerFactory()` gives you typed helpers such as `rc.json()`, `rc.text()`, and `rc.redirect()`
106
+ - Validation helpers such as `zValidator()` are optional
58
107
 
59
- ---
108
+ If you also want request validation with Zod, add `zValidator()`:
60
109
 
61
- ### 3. Generate Type Definitions with CLI
110
+ ```ts
111
+ import { routeHandlerFactory } from "rpc4next/server";
112
+ import { zValidator } from "rpc4next/server/validators/zod";
113
+ import { z } from "zod";
62
114
 
63
- CLI を利用して、Next.js のルート構造から型安全な RPC クライアントの定義を自動生成します。
115
+ const createRouteHandler = routeHandlerFactory();
64
116
 
65
- ```bash
66
- npx rpc4next <baseDir> <outputPath>
117
+ const querySchema = z.object({
118
+ includePosts: z.enum(["true", "false"]).optional(),
119
+ });
120
+
121
+ export const { GET } = createRouteHandler<{
122
+ params: { userId: string };
123
+ query: z.infer<typeof querySchema>;
124
+ }>().get(zValidator("query", querySchema), async (rc) => {
125
+ const query = rc.req.valid("query");
126
+ return rc.json({ ok: true, includePosts: query.includePosts === "true" });
127
+ });
67
128
  ```
68
129
 
69
- `rpc4next` command is provided by the `rpc4next-cli` package.
130
+ `zValidator()` validates request input and returns `400` JSON errors by default on invalid input.
70
131
 
71
- - `<baseDir>`: Next.js Appルータが配置されたベースディレクトリ
72
- - `<outputPath>`: 生成された型定義ファイルの出力先
132
+ ### 2. Generate `PathStructure`
73
133
 
74
- #### オプション
134
+ Generate the client types from your `app` directory:
75
135
 
76
- - **ウォッチモード**
77
- ファイル変更を検知して自動的に再生成する場合は `--watch` or `-w` オプションを付けます。
136
+ ```bash
137
+ npx rpc4next app src/generated/rpc.ts
138
+ ```
78
139
 
79
- ```bash
80
- npx rpc4next <baseDir> <outputPath> --watch
81
- ```
140
+ If you use Bun:
82
141
 
83
- - **パラメータ型ファイルの生成**
84
- 各ルートに対して個別のパラメータ型定義ファイルを生成する場合は、`--params-file` or `-p` オプションにファイル名を指定します。
142
+ ```bash
143
+ bunx rpc4next app src/generated/rpc.ts
144
+ ```
85
145
 
86
- ```bash
87
- npx rpc4next <baseDir> <outputPath> --params-file <paramsFileName>
88
- ```
146
+ You can also configure the CLI with `rpc4next.config.json`:
89
147
 
90
- ---
148
+ ```json
149
+ {
150
+ "baseDir": "app",
151
+ "outputPath": "src/generated/rpc.ts",
152
+ "paramsFile": "params.ts"
153
+ }
154
+ ```
91
155
 
92
- ### 4. Create Your RPC Client
156
+ Then run:
93
157
 
94
- 生成された型定義ファイルを基に、RPC クライアントを作成します。
158
+ ```bash
159
+ npx rpc4next
160
+ ```
95
161
 
96
- ```ts
97
- // lib/rpcClient.ts
98
- import { createClient } from "rpc4next/client";
99
- import type { PathStructure } from "あなたが生成した型定義ファイル";
162
+ Or with Bun:
100
163
 
101
- export const rpc = createClient<PathStructure>();
164
+ ```bash
165
+ bunx rpc4next
102
166
  ```
103
167
 
104
- ---
168
+ Positional arguments:
105
169
 
106
- ### 5. Use It in Your Components
170
+ - `<baseDir>`: the App Router root to scan, such as `app`
171
+ - `<outputPath>`: the file to generate, such as `src/generated/rpc.ts`
107
172
 
108
- コンポーネント内で生成された RPC クライアントを使用します。
173
+ Useful options:
109
174
 
110
- ```tsx
111
- // app/page.tsx
112
- import { rpc } from "@/lib/rpcClient";
175
+ - `-w`, `--watch`: regenerate on file changes
176
+ - `-p`, `--params-file [filename]`: generate sibling params files such as `app/users/[userId]/params.ts`
113
177
 
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
- }
178
+ Examples:
179
+
180
+ ```bash
181
+ npx rpc4next --watch
182
+ npx rpc4next app src/generated/rpc.ts --params-file params.ts
121
183
  ```
122
184
 
123
- - エディタの補完機能により、利用可能なエンドポイントが自動的に表示されます。
124
- - リクエストの構造(params, query)はサーバーコードから推論され、レスポンスも型安全に扱えます。
185
+ ### 3. Create a Client
125
186
 
126
- ---
187
+ ```ts
188
+ // src/lib/rpc-client.ts
189
+ import { createRpcClient } from "rpc4next/client";
190
+ import type { PathStructure } from "../generated/rpc";
127
191
 
128
- ## さらに型安全にしたい場合 `createRouteHandler` による Next.js の型安全強化
192
+ export const rpc = createRpcClient<PathStructure>("");
193
+ ```
194
+
195
+ Use `""` for same-origin calls in the browser, or pass an absolute base URL for server-side or cross-origin usage.
129
196
 
130
- ### 📌 主なメリット
197
+ ### 4. Call Routes
131
198
 
132
- 1. **レスポンス型安全**
133
- - ステータス、Content-Type、Body がすべて型で保証される
134
- - クライアントは受け取るレスポンス型を完全に推論可能
199
+ Generated client naming follows the App Router path shape:
135
200
 
136
- 2. **クライアント側補完強化**
137
- - `status`, `content-type`, `json()`, `text()` などが適切に補完される
201
+ - static segments stay as property access, such as `rpc.api.users`
202
+ - dynamic segments become callable helpers, such as `[userId] -> ._userId("123")`
203
+ - `route.ts` methods become `$get()`, `$post()`, and so on
204
+ - `page.tsx` entries can be turned into typed URLs with `$url()`
138
205
 
139
- 3. **サーバー側 params / query も型安全**
140
- - `createRouteHandler()` + `zValidator()` を使えば、`params`, `query`, `headers`, `cookies`, `json` も型推論・バリデーション可能
141
- - `createRouteHandler()` + `zValidator()` を使えば、`Query` 型もexportする必要なし
206
+ ```ts
207
+ const response = await rpc.api.users._userId("123").$get({
208
+ url: { query: { includePosts: "true" } },
209
+ });
142
210
 
143
- ---
211
+ const data = await response.json();
212
+ ```
144
213
 
145
- ### 基本的な使い方
214
+ For JSON request bodies:
146
215
 
147
216
  ```ts
148
- const createRouteHandler = routeHandlerFactory((err, rc) =>
149
- rc.text("error", { status: 400 }),
150
- );
217
+ const response = await rpc.api.posts.$post({
218
+ body: { json: { title: "hello" } },
219
+ });
220
+ ```
221
+
222
+ For request headers and cookies:
223
+
224
+ ```ts
225
+ const response = await rpc.api["request-meta"].$get({
226
+ requestHeaders: {
227
+ headers: { "x-integration-test": "example" },
228
+ cookies: { session: "abc123" },
229
+ },
230
+ });
231
+ ```
232
+
233
+ ### 5. Generate Typed URLs for Pages
151
234
 
152
- const { POST } = createRouteHandler().post(async (rc) => rc.text("plain text"));
235
+ `page.tsx` files are included in the generated path tree, so you can build typed URLs even when there is no RPC method to call.
236
+
237
+ ```ts
238
+ const photoUrl = rpc.photo._id("42").$url();
239
+
240
+ photoUrl.path;
241
+ photoUrl.relativePath;
242
+ photoUrl.pathname;
243
+ photoUrl.params;
153
244
  ```
154
245
 
155
- これだけで、POSTリクエストの返り値に、レスポンスの内容 (`json`, `text`など)、`status`, `content-type` が型付けされるようになります。
246
+ ## Server Helpers
156
247
 
157
- ---
248
+ ### `routeHandlerFactory`
158
249
 
159
- ### 👤 サーバー側でのより型安全なルート作成
250
+ `routeHandlerFactory()` creates typed handlers for:
160
251
 
161
- `createRouteHandler()` `zValidator()` を使うことで、各リクエストパーツに対して **型安全なバリデーション** をかけられます。
252
+ - `get`
253
+ - `post`
254
+ - `put`
255
+ - `delete`
256
+ - `patch`
257
+ - `head`
258
+ - `options`
162
259
 
163
- #### シンプルな例
260
+ It also supports a shared error handler:
164
261
 
165
262
  ```ts
166
- import { createRouteHandler } from "@/path/to/createRouteHandler";
167
- import { zValidator } from "@/path/to/zValidator";
168
- import { z } from "zod";
263
+ import { routeHandlerFactory } from "rpc4next/server";
169
264
 
170
- // Zodスキーマを定義
171
- const paramsSchema = z.object({
172
- userId: z.string(),
265
+ const createRouteHandler = routeHandlerFactory((error, rc) => {
266
+ return rc.text("error", 400);
173
267
  });
174
268
 
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
- );
269
+ export const { POST } = createRouteHandler().post(async (rc) => {
270
+ return rc.json({ ok: true }, 201);
271
+ });
185
272
  ```
186
273
 
187
- ## ✅ サポートされているバリデーションターゲット
274
+ ### `zValidator`
188
275
 
189
- サーバー側では,次のリクエスト部分を型安全に検証できます:
276
+ `zValidator()` supports these targets:
190
277
 
191
- | ターゲット | 説明 |
192
- | :--------- | :-------------------------------------------------- |
193
- | `params` | URLパラメータ ( `/user/:id` の `id`など) |
194
- | `query` | クエリパラメータ (`?q=xxx&page=1`など) |
195
- | `headers` | リクエストヘッダー |
196
- | `cookies` | クッキー |
197
- | `json` | リクエストボディ (Content-Type: `application/json`) |
278
+ - `params`
279
+ - `query`
280
+ - `json`
281
+ - `headers`
282
+ - `cookies`
198
283
 
199
- ---
200
-
201
- ### 🔥 複数ターゲットを同時に検証する例
284
+ Example:
202
285
 
203
286
  ```ts
204
- import { createRouteHandler } from "@/path/to/createRouteHandler";
205
- import { zValidator } from "@/path/to/zValidator";
287
+ import { routeHandlerFactory } from "rpc4next/server";
288
+ import { zValidator } from "rpc4next/server/validators/zod";
206
289
  import { z } from "zod";
207
290
 
208
- const querySchema = z.object({
209
- page: z.string().regex(/^\d+$/),
210
- });
291
+ const createRouteHandler = routeHandlerFactory();
211
292
 
212
293
  const jsonSchema = z.object({
213
- name: z.string(),
214
- age: z.number(),
294
+ title: z.string().min(1),
215
295
  });
216
296
 
217
- export const { POST } = createRouteHandler<{
218
- query: z.infer<typeof querySchema>;
219
- }>().post(
220
- zValidator("query", querySchema),
297
+ export const { POST } = createRouteHandler().post(
221
298
  zValidator("json", jsonSchema),
222
299
  async (rc) => {
223
- const query = rc.req.valid("query");
224
300
  const body = rc.req.valid("json");
225
- return rc.json({ query, body });
301
+ return rc.json({ title: body.title }, 201);
226
302
  },
227
303
  );
228
304
  ```
229
305
 
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
- - 必要に応じて、**カスタムフック**でエラー対応を制御できます。
306
+ If you provide a custom hook, you must return a response yourself when validation fails:
243
307
 
244
308
  ```ts
245
- zValidator("params", paramsSchema, (result, rc) => {
309
+ zValidator("json", jsonSchema, (result, rc) => {
246
310
  if (!result.success) {
247
- return rc.json({ error: result.error.errors }, { status: 422 });
311
+ return rc.json({ error: result.error.issues }, 422);
248
312
  }
249
313
  });
250
314
  ```
251
315
 
252
- > (フック内でレスポンスを返さない場合は、通常通り例外がスローされます)
316
+ ## Plain Next.js Route Handlers Also Work
253
317
 
254
- ---
318
+ You can keep using native App Router handlers without adopting `routeHandlerFactory()`.
319
+ This is useful when you want to stay close to stock Next.js APIs and only use `rpc4next` for route scanning and client generation.
255
320
 
256
- ## 📡 クライアント側での使い方
257
-
258
- `rpc4next`で作成したクライアントは、`createRouteHandler` と `zValidator` で作成したルートハンドラの内容にしたがって **params, query, headers, cookies, json** を型安全に送信できます。
259
-
260
- 例:
321
+ Example with `NextResponse.json(...)`:
261
322
 
262
323
  ```ts
263
- import { createRpcClient } from "@/path/to/rpc-client";
264
- import type { PathStructure } from "@/path/to/generated-types";
324
+ // app/api/next-native/[itemId]/route.ts
325
+ import { type NextRequest, NextResponse } from "next/server";
265
326
 
266
- const client = createRpcClient<PathStructure>("http://localhost:3000");
327
+ export type Query = {
328
+ filter?: string;
329
+ };
330
+
331
+ export async function GET(
332
+ request: NextRequest,
333
+ context: { params: Promise<{ itemId: string }> },
334
+ ) {
335
+ const { itemId } = await context.params;
336
+ const filter = request.nextUrl.searchParams.get("filter") ?? "all";
267
337
 
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" } },
338
+ return NextResponse.json({
339
+ ok: true,
340
+ itemId,
341
+ filter,
272
342
  });
343
+ }
344
+ ```
273
345
 
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
- }
346
+ Example with `Response.json(...)`:
347
+
348
+ ```ts
349
+ // app/api/next-native-response/route.ts
350
+ export async function GET() {
351
+ return Response.json({
352
+ ok: true,
353
+ source: "response-json",
354
+ });
300
355
  }
301
356
  ```
302
357
 
303
- - エディタの補完機能により、送信できるターゲットが明示されます
304
- - サーバー側の型定義に基づいて、**型のズレを防止**できます
358
+ The generated client can still call this route:
305
359
 
306
- ---
360
+ ```ts
361
+ const response = await rpc.api["next-native"]
362
+ ._itemId("item-1")
363
+ .$get({ url: { query: { filter: "recent" } } });
364
+ ```
365
+
366
+ You can also call a plain `Response.json(...)` route:
367
+
368
+ ```ts
369
+ const response = await rpc.api["next-native-response"].$get();
370
+ ```
371
+
372
+ For native handlers, route discovery and request typing still work, but response typing is naturally broader than when you return rpc4next's typed helpers.
373
+
374
+ See [integration/next-app/README.md](./integration/next-app/README.md) for the repository's full integration fixture coverage and route-pattern notes.
375
+
376
+ ## Generated Files
377
+
378
+ When `paramsFile` is enabled, the CLI can generate sibling files such as:
379
+
380
+ ```ts
381
+ // app/api/users/[userId]/params.ts
382
+ export type Params = { userId: string };
383
+ ```
307
384
 
308
- これらのように、リクエスト時にはさまざまなターゲット (`params`, `query`, `headers`, `cookies`, `json`) を送信できます。
385
+ That lets route files import the param shape instead of repeating it manually.
386
+ These generated `params.ts` files are optional, and your generated `src/generated/rpc.ts` is typically not something you edit by hand.
309
387
 
310
- さらに、サーバー側では、これらを**個別に型付け、バリデーション**できます。
388
+ Your generated `src/generated/rpc.ts` exports a `PathStructure` type that includes:
311
389
 
312
- ---
390
+ - path entries from `page.tsx`
391
+ - callable HTTP methods from `route.ts`
392
+ - dynamic segment parameter types
393
+ - route `Query` exports where available
313
394
 
314
- ## 🧭 Monorepo Layout
395
+ ## Typical Workflow
315
396
 
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`
397
+ 1. Add or update files under `app/**`
398
+ 2. Run `rpc4next` to regenerate `PathStructure`
399
+ 3. Import `PathStructure` into your client
400
+ 4. Call routes with `createRpcClient<PathStructure>(...)`
401
+ 5. Use `routeHandlerFactory` and `zValidator` where you want stronger server-side typing
322
402
 
323
- ## 🚧 Requirements
403
+ ## Repository Layout
324
404
 
325
- - Next.js 14+ (App Router 使用)
326
- - Node.js 20.9.0+
405
+ - `packages/rpc4next`: runtime client and server helpers
406
+ - `packages/rpc4next-cli`: route scanner and type generator
407
+ - `packages/rpc4next-shared`: internal shared constants and types
408
+ - `integration/next-app`: real Next.js integration fixture
327
409
 
328
- ---
410
+ If you are evaluating the repository itself, `integration/next-app` is the best place to see the full flow working in a real app.
329
411
 
330
- ## 💼 License
412
+ ## License
331
413
 
332
414
  MIT
@@ -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{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
+ 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 +1 @@
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
+ 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 +1 @@
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
+ 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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rpc4next",
3
- "version": "0.5.0",
3
+ "version": "0.6.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
5
  "keywords": [
6
6
  "next.js",
@@ -16,6 +16,9 @@
16
16
  "license": "MIT",
17
17
  "author": "watanabe-1",
18
18
  "type": "module",
19
+ "engines": {
20
+ "node": ">=20.19.2"
21
+ },
19
22
  "module": "dist/index.js",
20
23
  "types": "dist/index.d.ts",
21
24
  "files": [
@@ -93,13 +96,13 @@
93
96
  "test:watch": "vitest --watch",
94
97
  "lint": "biome check src",
95
98
  "lint:fix": "biome check --write src",
96
- "typecheck": "tsc -b tsconfig.build.json --noEmit"
99
+ "typecheck": "tsc -p tsconfig.build.json --noEmit"
97
100
  },
98
101
  "dependencies": {
99
- "rpc4next-shared": "^0.2.0"
102
+ "rpc4next-shared": "^0.3.0"
100
103
  },
101
104
  "peerDependencies": {
102
- "next": "^14.0.0 || ^15.0.0"
105
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0"
103
106
  },
104
107
  "publishConfig": {
105
108
  "access": "public"