rpc4next-cli 0.1.5 → 0.3.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +332 -0
  3. package/dist/index.js +20 -6
  4. package/package.json +9 -10
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.js CHANGED
@@ -1,12 +1,26 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Ht}from"commander";import Pt from"path";var A=["page.tsx","route.ts"],w=0,P=1,F=1,W=20,M="\u2192";import Y from"path";var tt=(t,r)=>{let e=x(Y.relative(Y.dirname(t),r)).replace(/\.tsx?$/,"");return e.startsWith("../")||(e="./"+e),e},x=t=>t.replace(/\\/g,"/"),_=t=>Y.relative(process.cwd(),t);import yt from"fs";import Ut from"path";var et=["Query"];var U="Endpoint",j="QueryKey",H="ParamsKey",rt=[U,H,j],ot="rpc4next/client";import ut from"fs";import v from"path";import{CATCH_ALL_PREFIX as Ot,DYNAMIC_PREFIX as wt,HTTP_METHODS_EXCLUDE_OPTIONS as Ft,OPTIONAL_CATCH_ALL_PREFIX as Wt}from"rpc4next-shared";import G from"path";var E=new Map,C=new Map,nt=(t,r)=>{let e=G.resolve(r);[...t.keys()].forEach(o=>{let n=G.resolve(o);(n===e||e.startsWith(n+G.sep))&&t.delete(o)})},st=t=>{nt(E,t)},it=t=>{nt(C,t)};import Dt from"fs";import vt from"crypto";var at=(t,r)=>{let e=vt.createHash("md5").update(`${t}::${r}`).digest("hex").slice(0,16);return`${r}_${e}`};var R=(t,r)=>!t||!r?"":`Record<${t}, ${r}>`,b=t=>t.length===0||t.some(({name:r,type:e})=>!r||!e)?"":`{ ${t.map(({name:r,type:e})=>`"${r}": ${e}`).join(`${";"} `)}${t.length>1?";":""} }`,L=(t,r,e)=>!t||!r?"":e?`import type { ${t} as ${e} } from "${r}"${";"}`:`import type { ${t} } from "${r}"${";"}`;var ct=(t,r,e,o)=>{let n=Dt.readFileSync(r,"utf8"),i=e(n);if(!i)return;let s=tt(t,r),p=at(s,i);return{importName:p,importPath:s,importStatement:L(i,s,p),type:o(i,p)}},mt=(t,r)=>ct(t,r,e=>et.find(o=>new RegExp(`export (interface ${o} ?{|type ${o} ?=)`).test(e)),(e,o)=>R(j,o)),lt=(t,r,e)=>ct(t,r,o=>[e].find(n=>new RegExp(`export (async )?(function ${n} ?\\(|const ${n} ?=|\\{[^}]*\\b${n}\\b[^}]*\\} ?=|const \\{[^}]*\\b${n}\\b[^}]*\\} ?=|\\{[^}]*\\b${n}\\b[^}]*\\} from)`).test(o)),(o,n)=>b([{name:`$${o.toLowerCase()}`,type:`typeof ${n}`}]));var ft=new Set(A),ht=t=>{if(E.has(t))return E.get(t);let r=ut.readdirSync(t,{withFileTypes:!0});for(let e of r){let{name:o}=e,n=v.join(t,o);if(o==="node_modules"||o.startsWith("_")||o.startsWith("(.)")||o.startsWith("(..)")||o.startsWith("(...)"))return E.set(t,!1),!1;if(e.isFile()&&ft.has(o))return E.set(t,!0),!0;if(e.isDirectory()&&ht(n))return E.set(t,!0),!0}return E.set(t,!1),!1},Mt=(t,{isDynamic:r,isCatchAll:e,isOptionalCatchAll:o})=>{let n=t;return r&&(n=n.replace(/^\[+|\]+$/g,"")),(e||o)&&(n=n.replace(/^\.{3}/,"")),{paramName:n,keyName:`${o?Wt:e?Ot:r?wt:""}${n}`}},k=(t,r,e="",o=[])=>{if(C.has(r))return C.get(r);let n=e,i=e+" ",s=[],p=[],a=[],m=[],c=[...o],$=ut.readdirSync(r,{withFileTypes:!0}).filter(d=>{if(d.isDirectory()){let y=v.join(r,d.name);return ht(y)}return ft.has(d.name)}).sort();for(let d of $){let y=x(v.join(r,d.name));if(d.isDirectory()){let l=d.name,h=l.startsWith("(")&&l.endsWith(")"),u=l.startsWith("@"),f=l.startsWith("[[...")&&l.endsWith("]]"),g=l.startsWith("[...")&&l.endsWith("]"),S=l.startsWith("[")&&l.endsWith("]"),{paramName:It,keyName:At}=Mt(l,{isDynamic:S,isCatchAll:g,isOptionalCatchAll:f}),Rt=S||g||f?[...c,{paramName:It,routeType:{isDynamic:S,isCatchAll:g,isOptionalCatchAll:f,isGroup:h,isParallel:u}}]:c,z=h||u,{pathStructure:O,imports:bt,paramsTypes:Lt}=k(t,y,z?n:i,Rt);if(p.push(...bt),m.push(...Lt),z){let J=O.match(/^\s*\{([\s\S]*)\}\s*$/),Z=O.trim();if(J)s.push(`${i}${J[1].trim()}`);else if(Z)a.push(Z);else throw new Error(`Invalid empty child path structure in grouped/parallel route: ${y}`)}else s.push(`${i}"${At}": ${O}`)}else{let l=mt(t,y);if(l){let{importStatement:h,importPath:u,type:f}=l;p.push({statement:h,path:u}),a.push(f)}if(Ft.forEach(h=>{let u=lt(t,y,h);if(u){let{importStatement:f,importPath:g,type:S}=u;p.push({statement:f,path:g}),a.push(S)}}),a.push(U),c.length>0){let h=c.map(({paramName:f,routeType:g})=>{let S=g.isCatchAll?"string[]":g.isOptionalCatchAll?"string[] | undefined":"string";return{name:f,type:S}}),u=b(h);m.push({paramsType:u,dirPath:v.dirname(y)}),a.push(R(H,u))}}}let I=a.join(" & "),D=s.length>0?`{${`
3
- `}${s.join(`,${`
2
+ import Gt from"node:path";import{parseArgs as Xt}from"node:util";import _t from"node:path";var A=["page.tsx","route.ts"];import O from"node:path";var J=(t,e)=>{let r=P(O.relative(O.dirname(t),e)).replace(/\.tsx?$/,"");return r.startsWith("../")||(r=`./${r}`),r},P=t=>t.replace(/\\/g,"/"),_=t=>O.relative(process.cwd(),t);import xt from"node:fs";import Ht from"node:path";var Z=["Query"];var F="Endpoint",W="QueryKey",M="ParamsKey",tt=[F,M,W],et="rpc4next/client";import mt from"node:fs";import L from"node:path";import{CATCH_ALL_PREFIX as Ft,DYNAMIC_PREFIX as Wt,HTTP_METHODS_EXCLUDE_OPTIONS as Mt,OPTIONAL_CATCH_ALL_PREFIX as Yt}from"rpc4next-shared";import Y from"node:path";var S=new Map,N=new Map,rt=(t,e)=>{let r=Y.resolve(e);[...t.keys()].forEach(n=>{let o=Y.resolve(n);(o===r||r.startsWith(o+Y.sep))&&t.delete(n)})},ot=t=>{rt(S,t)},nt=t=>{rt(N,t)};import Ot from"node:fs";import Dt from"node:crypto";var st=(t,e)=>{let r=Dt.createHash("md5").update(`${t}::${e}`).digest("hex").slice(0,16);return`${e}_${r}`};var I=(t,e)=>!t||!e?"":`Record<${t}, ${e}>`,b=t=>t.length===0||t.some(({name:e,type:r})=>!e||!r)?"":`{ ${t.map(({name:e,type:r})=>`"${e}": ${r}`).join(`${";"} `)}${t.length>1?";":""} }`,R=(t,e,r)=>!t||!e?"":r?`import type { ${t} as ${r} } from "${e}"${";"}`:`import type { ${t} } from "${e}"${";"}`;var at=(t,e,r,n)=>{let o=Ot.readFileSync(e,"utf8"),s=r(o);if(!s)return;let i=J(t,e),a=st(i,s);return{importName:a,importPath:i,importStatement:R(s,i,a),type:n(s,a)}},pt=(t,e)=>at(t,e,r=>Z.find(n=>new RegExp(`export (interface ${n} ?{|type ${n} ?=)`).test(r)),(r,n)=>I(W,n)),ct=(t,e,r)=>at(t,e,n=>[r].find(o=>new RegExp(`export (async )?(function ${o} ?\\(|const ${o} ?=|\\{[^}]*\\b${o}\\b[^}]*\\} ?=|const \\{[^}]*\\b${o}\\b[^}]*\\} ?=|\\{[^}]*\\b${o}\\b[^}]*\\} from)`).test(n)),(n,o)=>b([{name:`$${n.toLowerCase()}`,type:`typeof ${o}`}]));var ut=new Set(A),lt=t=>{let e=S.get(t);if(e!==void 0)return e;let r=mt.readdirSync(t,{withFileTypes:!0});for(let n of r){let{name:o}=n,s=L.join(t,o);if(o==="node_modules"||o.startsWith("_")||o.startsWith("(.)")||o.startsWith("(..)")||o.startsWith("(...)"))return S.set(t,!1),!1;if(n.isFile()&&ut.has(o))return S.set(t,!0),!0;if(n.isDirectory()&&lt(s))return S.set(t,!0),!0}return S.set(t,!1),!1},Ut=(t,{isDynamic:e,isCatchAll:r,isOptionalCatchAll:n})=>{let o=t;return e&&(o=o.replace(/^\[+|\]+$/g,"")),(r||n)&&(o=o.replace(/^\.{3}/,"")),{paramName:o,keyName:`${n?Yt:r?Ft:e?Wt:""}${o}`}},j=(t,e,r="",n=[])=>{let o=N.get(e);if(o!==void 0)return o;let s=r,i=r+" ",a=[],p=[],c=[],m=[],h=[...n],Q=mt.readdirSync(e,{withFileTypes:!0}).filter(E=>{if(E.isDirectory()){let T=L.join(e,E.name);return lt(T)}return ut.has(E.name)}).sort();for(let E of Q){let T=P(L.join(e,E.name));if(E.isDirectory()){let u=E.name,g=u.startsWith("(")&&u.endsWith(")"),l=u.startsWith("@"),f=u.startsWith("[[...")&&u.endsWith("]]"),d=u.startsWith("[...")&&u.endsWith("]"),x=u.startsWith("[")&&u.endsWith("]"),{paramName:bt,keyName:Rt}=Ut(u,{isDynamic:x,isCatchAll:d,isOptionalCatchAll:f}),Lt=x||d||f?[...h,{paramName:bt,routeType:{isDynamic:x,isCatchAll:d,isOptionalCatchAll:f,isGroup:g,isParallel:l}}]:h,V=g||l,{pathStructure:D,imports:vt,paramsTypes:wt}=j(t,T,V?s:i,Lt);if(p.push(...vt),m.push(...wt),V){let q=D.match(/^\s*\{([\s\S]*)\}\s*$/),z=D.trim();if(q)a.push(`${i}${q[1].trim()}`);else if(z)c.push(z);else throw new Error(`Invalid empty child path structure in grouped/parallel route: ${T}`)}else a.push(`${i}"${Rt}": ${D}`)}else{let u=pt(t,T);if(u){let{importStatement:g,importPath:l,type:f}=u;p.push({statement:g,path:l}),c.push(f)}if(Mt.forEach(g=>{let l=ct(t,T,g);if(l){let{importStatement:f,importPath:d,type:x}=l;p.push({statement:f,path:d}),c.push(x)}}),c.push(F),h.length>0){let g=h.map(({paramName:f,routeType:d})=>{let x=d.isCatchAll?"string[]":d.isOptionalCatchAll?"string[] | undefined":"string";return{name:f,type:x}}),l=b(g);m.push({paramsType:l,dirPath:L.dirname(T)}),c.push(I(M,l))}}}let v=c.join(" & "),w=a.length>0?`{${`
3
+ `}${a.join(`,${`
4
4
  `}`)}${`
5
- `}${n}}`:"",q={pathStructure:I&&D?`${I} & ${D}`:I||D,imports:p,paramsTypes:m};return C.set(r,q),q};var gt=(t,r)=>{let{pathStructure:e,imports:o,paramsTypes:n}=k(t,r),i=`export type PathStructure = ${e}${";"}`,s=o.length?`${o.sort((c,$)=>c.path.localeCompare($.path,void 0,{numeric:!0})).map(c=>c.statement).join(`
6
- `)}`:"",p=rt.filter(c=>e.includes(c)),a=L(p.join(" ,"),ot),m=n.map(({paramsType:c,dirPath:$})=>({paramsType:`export type Params = ${c}${";"}`,dirPath:$}));return{pathStructure:`${a}${`
7
- `}${s}${`
5
+ `}${s}}`:"",B={pathStructure:v&&w?`${v} & ${w}`:v||w,imports:p,paramsTypes:m};return N.set(e,B),B};var ft=(t,e)=>{let{pathStructure:r,imports:n,paramsTypes:o}=j(t,e),s=`export type PathStructure = ${r}${";"}`,i=n.length?`${n.sort((m,h)=>m.path.localeCompare(h.path,void 0,{numeric:!0})).map(m=>m.statement).join(`
6
+ `)}`:"",a=tt.filter(m=>r.includes(m)),p=R(a.join(" ,"),et),c=o.map(({paramsType:m,dirPath:h})=>({paramsType:`export type Params = ${m}${";"}`,dirPath:h}));return{pathStructure:`${p}${`
7
+ `}${i}${`
8
8
  `}${`
9
- `}${i}`,paramsTypes:m}};var Yt=()=>!!process.stdout?.isTTY;var K=t=>r=>Yt()?`\x1B[${t}m${r}\x1B[0m`:r,Et=K(36),Tt=K(32),Q=K(31);var V=(t,r,e="\u2192",o=24)=>t.padEnd(o)+` ${e} ${r}`,B=(t=0)=>" ".repeat(t),dt=()=>({info:(t,r={})=>{let{indentLevel:e=0,event:o}=r,n=o?`${Et(`[${o}]`)} `:"";console.log(`${B(e)}${n}${t}`)},success:(t,r={})=>{let{indentLevel:e=0}=r;console.log(`${B(e)}${Tt("\u2713")} ${t}`)},error:(t,r={})=>{let{indentLevel:e=0}=r;console.error(`${B(e)}${Q("\u2717")} ${Q(t)}`)}});var St=({baseDir:t,outputPath:r,paramsFileName:e,logger:o})=>{o.info("Generating types...",{event:"generate"});let{pathStructure:n,paramsTypes:i}=gt(r,t);yt.writeFileSync(r,n),o.success(V("Path structure type",_(r),M,W),{indentLevel:F}),e&&(i.forEach(({paramsType:s,dirPath:p})=>{let a=Ut.join(p,e);yt.writeFileSync(a,s)}),o.success(V("Params types",e,M,W),{indentLevel:F}))};import jt from"chokidar";var xt=(t,r)=>{let e=null,o=!1,n=null,i=async(...s)=>{o=!0;try{await t(...s)}finally{if(o=!1,n){let p=n;n=null,i(...p)}}};return(...s)=>{e&&clearTimeout(e),e=setTimeout(()=>{if(o){n=s;return}i(...s)},r)}};var $t=(t,r,e)=>{e.info(`${_(t)}`,{event:"watch"});let o=a=>A.some(m=>a.endsWith(m)),n=new Set,i=xt(()=>{n.forEach(a=>{st(a),it(a)}),n.clear(),r()},300),s=jt.watch(t,{ignoreInitial:!0,ignored:(a,m)=>!!m?.isFile()&&!o(a)});s.on("ready",()=>{i(),s.on("all",(a,m)=>{if(o(m)){let c=_(m);e.info(c,{event:a}),n.add(m),i()}})}),s.on("error",a=>{a instanceof Error?e.error(`Watcher error: ${a.message}`):e.error(`Unknown watcher error: ${String(a)}`)});let p=()=>{s.close().then(()=>{e.info("Watcher closed.",{event:"watch"})}).catch(a=>{e.error(`Failed to close watcher: ${a.message}`)})};process.on("SIGINT",p),process.on("SIGTERM",p)};var _t=(t,r,e,o)=>{try{return St({baseDir:t,outputPath:r,paramsFileName:e,logger:o}),w}catch(n){return n instanceof Error?o.error(`Failed to generate: ${n.message}`):o.error(`Unknown error occurred during generate: ${String(n)}`),P}},Ct=(t,r,e,o)=>{let n=x(Pt.resolve(t)),i=x(Pt.resolve(r)),s=typeof e.paramsFile=="string"?e.paramsFile:null;return e.paramsFile!==void 0&&!s?(o.error("Error: --params-file requires a filename."),P):e.watch?($t(n,()=>{_t(n,i,s,o)},o),w):_t(n,i,s,o)};var Nt=(t,r=dt())=>{let e=new Ht;e.description("Generate RPC client type definitions based on the Next.js path structure.").argument("<baseDir>","Base directory containing Next.js paths for type generation").argument("<outputPath>","Output path for the generated type definitions").option("-w, --watch","Watch mode: regenerate on file changes").option("-p, --params-file [filename]","Generate params types file with specified filename").action(async(o,n,i)=>{try{let s=await Ct(o,n,i,r);i.watch||process.exit(s)}catch(s){r.error(`Unexpected error occurred:${s instanceof Error?s.message:String(s)}`),process.exit(P)}}),e.parse(t)};Nt(process.argv);
9
+ `}${s}`,paramsTypes:c}};var jt=()=>!!process.stdout?.isTTY;var H=t=>e=>jt()?`\x1B[${t}m${e}\x1B[0m`:e,ht=H(36),gt=H(32),k=H(31);var X=(t,e,r="\u2192",n=24)=>`${t.padEnd(n)} ${r} ${e}`,G=(t=0)=>" ".repeat(t),dt=()=>({info:(t,e={})=>{let{indentLevel:r=0,event:n}=e,o=n?`${ht(`[${n}]`)} `:"";console.log(`${G(r)}${o}${t}`)},success:(t,e={})=>{let{indentLevel:r=0}=e;console.log(`${G(r)}${gt("\u2713")} ${t}`)},error:(t,e={})=>{let{indentLevel:r=0}=e;console.error(`${G(r)}${k("\u2717")} ${k(t)}`)}});var St=({baseDir:t,outputPath:e,paramsFileName:r,logger:n})=>{n.info("Generating types...",{event:"generate"});let{pathStructure:o,paramsTypes:s}=ft(e,t);xt.writeFileSync(e,o),n.success(X("Path structure type",_(e),"\u2192",20),{indentLevel:1}),r&&(s.forEach(({paramsType:i,dirPath:a})=>{let p=Ht.join(a,r);xt.writeFileSync(p,i)}),n.success(X("Params types",r,"\u2192",20),{indentLevel:1}))};import kt from"chokidar";var Pt=(t,e)=>{let r=null,n=!1,o=null,s=async(...i)=>{n=!0;try{await t(...i)}finally{if(n=!1,o){let a=o;o=null,s(...a)}}};return(...i)=>{r&&clearTimeout(r),r=setTimeout(()=>{if(n){o=i;return}s(...i)},e)}};var $t=(t,e,r)=>{r.info(`${_(t)}`,{event:"watch"});let n=p=>A.some(c=>p.endsWith(c)),o=new Set,s=Pt(async()=>{let p=Array.from(o);o.clear();for(let c of p)ot(c),nt(c);await e()},300),i=kt.watch(t,{ignoreInitial:!0,ignored:(p,c)=>!!c?.isFile()&&!n(p)});i.on("ready",()=>{s(),i.on("all",(p,c)=>{if(n(c)){let m=_(c);r.info(m,{event:p}),o.add(c),s()}})}),i.on("error",p=>{p instanceof Error?r.error(`Watcher error: ${p.message}`):r.error(`Unknown watcher error: ${String(p)}`)});let a=()=>{i.close().then(()=>{r.info("Watcher closed.",{event:"watch"})}).catch(p=>{r.error(`Failed to close watcher: ${p.message}`)})};process.on("SIGINT",a),process.on("SIGTERM",a)};var Ct=(t,e,r,n)=>{try{return St({baseDir:t,outputPath:e,paramsFileName:r,logger:n}),0}catch(o){return o instanceof Error?n.error(`Failed to generate: ${o.message}`):n.error(`Unknown error occurred during generate: ${String(o)}`),1}},Nt=(t,e,r,n)=>{let o=P(_t.resolve(t)),s=P(_t.resolve(e)),i=typeof r.paramsFile=="string"?r.paramsFile:null;return r.paramsFile!==void 0&&!i?(n.error("Error: --params-file requires a filename."),1):r.watch?($t(o,()=>{Ct(o,s,i,n)},n),0):Ct(o,s,i,n)};function Kt(t){if(t.length===0)return[];if(t[0].startsWith("-"))return t;let e=Gt.basename(t[0]).toLowerCase();return new Set(["node","node.exe","bun","bun.exe","deno","deno.exe"]).has(e)&&t.length>=2?t.slice(2):t}function K(t){let e=`
10
+ Generate RPC client type definitions based on the Next.js path structure.
11
+
12
+ Usage:
13
+ rpc4next <baseDir> <outputPath> [options]
14
+
15
+ Arguments:
16
+ baseDir Base directory containing Next.js paths for type generation
17
+ outputPath Output path for the generated type definitions
18
+
19
+ Options:
20
+ -w, --watch Watch mode: regenerate on file changes
21
+ -p, --params-file [filename] Generate params types file (optional filename)
22
+ -h, --help Show help
23
+ `.trim();t.info(e)}function Qt(t,e){let r=[],n;for(let o=0;o<t.length;o++){let s=t[o],i=!1;for(let a of e)if(s.startsWith(`${a}=`)){n=s.slice(`${a}=`.length)||!0,i=!0;break}if(!i){if(e.includes(s)){let a=t[o+1];typeof a=="string"&&!a.startsWith("-")?(n=a,o++):n=!0;continue}r.push(s)}}return{args:r,value:n}}var It=(t,e=dt())=>{try{let r=Kt(t),{args:n,value:o}=Qt(r,["-p","--params-file"]),{values:s,positionals:i}=Xt({args:n,options:{watch:{type:"boolean",short:"w"},help:{type:"boolean",short:"h"}},allowPositionals:!0,strict:!0});s.help&&(K(e),process.exit(0));let a=i[0],p=i[1];(!a||!p)&&(e.error("Missing required arguments: <baseDir> <outputPath>"),K(e),process.exit(1));let c={watch:!!s.watch,...o!==void 0?{paramsFile:o}:{}};(async()=>{try{let m=await Nt(a,p,c,e);c.watch||process.exit(m)}catch(m){e.error(`Unexpected error occurred:${m instanceof Error?m.message:String(m)}`),process.exit(1)}})()}catch(r){e.error(r instanceof Error?r.message:`Invalid arguments: ${String(r)}`),K(e),process.exit(1)}};It(process.argv);
10
24
  /*!
11
25
  * Inspired by pathpida (https://github.com/aspida/pathpida),
12
26
  * especially the design and UX of its CLI.
package/package.json CHANGED
@@ -1,18 +1,15 @@
1
1
  {
2
2
  "name": "rpc4next-cli",
3
- "version": "0.1.5",
3
+ "version": "0.3.0",
4
4
  "description": "Command line interface for rpc4next. Generates RPC client type definitions from Next.js routes.",
5
- "author": "watanabe-1",
6
- "license": "MIT",
7
5
  "homepage": "https://github.com/watanabe-1/rpc4next#readme",
8
6
  "repository": {
9
7
  "type": "git",
10
8
  "url": "git+https://github.com/watanabe-1/rpc4next.git",
11
9
  "directory": "packages/rpc4next-cli"
12
10
  },
13
- "publishConfig": {
14
- "access": "public"
15
- },
11
+ "license": "MIT",
12
+ "author": "watanabe-1",
16
13
  "type": "module",
17
14
  "bin": {
18
15
  "rpc4next": "dist/index.js"
@@ -27,16 +24,18 @@
27
24
  "test:coverage": "vitest run --coverage.enabled true",
28
25
  "test:ui": "vitest --ui --coverage.enabled true",
29
26
  "test:watch": "vitest --watch",
30
- "lint": "eslint src",
31
- "lint:fix": "eslint src --fix",
27
+ "lint": "biome check src",
28
+ "lint:fix": "biome check --write src",
32
29
  "typecheck": "tsc --noEmit"
33
30
  },
34
31
  "dependencies": {
35
32
  "chokidar": "^5.0.0",
36
- "commander": "^14.0.3",
37
- "rpc4next-shared": "^0.1.5"
33
+ "rpc4next-shared": "^0.2.0"
38
34
  },
39
35
  "peerDependencies": {
40
36
  "next": "^14.0.0 || ^15.0.0"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
41
40
  }
42
41
  }