rpc4next-cli 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/index.js +10 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,18 @@ Inspired by Hono RPC and Pathpida, **rpc4next** automatically generates a type-s
|
|
|
8
8
|
|
|
9
9
|
---
|
|
10
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
|
+
|
|
11
23
|
## ✨ Features
|
|
12
24
|
|
|
13
25
|
- ✅ ルート、パラメータ、クエリパラメータ、 リクエストボディ、レスポンスの型安全なクライアント生成
|
|
@@ -68,6 +80,21 @@ npx rpc4next <baseDir> <outputPath>
|
|
|
68
80
|
|
|
69
81
|
`rpc4next` command is provided by the `rpc4next-cli` package.
|
|
70
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
|
+
|
|
71
98
|
- `<baseDir>`: Next.js の Appルータが配置されたベースディレクトリ
|
|
72
99
|
- `<outputPath>`: 生成された型定義ファイルの出力先
|
|
73
100
|
|
|
@@ -330,7 +357,7 @@ async function callUserApi() {
|
|
|
330
357
|
aqua i
|
|
331
358
|
```
|
|
332
359
|
|
|
333
|
-
- `aqua.yaml` を更新したら、チェックサムを更新してください
|
|
360
|
+
- `aqua/aqua.yaml` を更新したら、チェックサムを更新してください
|
|
334
361
|
|
|
335
362
|
```bash
|
|
336
363
|
aqua update-checksum
|
package/dist/index.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
`}${
|
|
2
|
+
import ce from"node:path";import{parseArgs as pe}from"node:util";import bt from"node:path";var D=["page.tsx","route.ts"];import G from"node:path";var it=(t,e)=>{let r=P(G.relative(G.dirname(t),e)).replace(/\.tsx?$/,"");return r.startsWith("../")||(r=`./${r}`),r},P=t=>t.replace(/\\/g,"/"),x=t=>G.relative(process.cwd(),t);import tt from"node:fs";import oe from"node:path";var at=["Query"];var k="Endpoint",K="QueryKey",X="ParamsKey",ct=[k,X,K],pt="rpc4next/client";import yt from"node:fs";import _ from"node:path";import{CATCH_ALL_PREFIX as Vt,DYNAMIC_PREFIX as zt,OPTIONAL_CATCH_ALL_PREFIX as Jt}from"rpc4next-shared";import Q from"node:path";var S=new Map,F=new Map,mt=(t,e)=>{let r=Q.resolve(e);[...t.keys()].forEach(n=>{let o=Q.resolve(n);(o===r||r.startsWith(o+Q.sep))&&t.delete(n)})},ut=t=>{mt(S,t)},lt=t=>{mt(F,t)};import Xt from"node:fs";import{HTTP_METHODS_EXCLUDE_OPTIONS as Qt}from"rpc4next-shared";import Kt from"node:crypto";var ft=(t,e)=>{let r=Kt.createHash("md5").update(`${t}::${e}`).digest("hex").slice(0,16);return`${e}_${r}`};var v=(t,e)=>!t||!e?"":`Record<${t}, ${e}>`,L=t=>t.length===0||t.some(({name:e,type:r})=>!e||!r)?"":`{ ${t.map(({name:e,type:r})=>`"${e}": ${r}`).join(`${";"} `)}${t.length>1?";":""} }`,O=(t,e,r)=>!t||!e?"":r?`import type { ${t} as ${r} } from "${e}"${";"}`:`import type { ${t} } from "${e}"${";"}`;var gt=(t,e,r,n)=>{let o=it(t,e),i=ft(o,r);return{importName:i,importPath:o,importStatement:O(r,o,i),type:n(r,i),exportName:r}},qt=t=>at.find(e=>new RegExp(`export (interface ${e} ?{|type ${e} ?=)`).test(t)),Bt=(t,e)=>new RegExp(`export (async )?(function ${e} ?\\(|const ${e} ?=|\\{[^}]*\\b${e}\\b[^}]*\\} ?=|const \\{[^}]*\\b${e}\\b[^}]*\\} ?=|\\{[^}]*\\b${e}\\b[^}]*\\} from)`).test(t),dt=(t,e)=>{let r=Xt.readFileSync(e,"utf8"),n=qt(r),o=n?gt(t,e,n,(s,a)=>v(K,a)):void 0,i=Qt.filter(s=>Bt(r,s)).map(s=>gt(t,e,s,(a,c)=>L([{name:`$${a.toLowerCase()}`,type:`typeof ${c}`}])));return{...o?{query:o}:{},routes:i}};var Et=new Set(D),Tt=["(..)(..)","(...)","(..)","(.)"],Zt=t=>Tt.some(e=>t.startsWith(e)),te=t=>{let e=t;for(;;){let r=Tt.find(n=>e.startsWith(n));if(!r)return e;e=e.slice(r.length)}},ee=t=>{try{let e=decodeURIComponent(t);return e.startsWith("_")?t:e}catch{return t}},q=t=>{let e=Zt(t),r=e?te(t):t,n=!e&&r.startsWith("(")&&r.endsWith(")"),o=!e&&r.startsWith("@"),i=r.startsWith("[[...")&&r.endsWith("]]"),s=!i&&r.startsWith("[...")&&r.endsWith("]"),a=r.startsWith("[")&&r.endsWith("]"),c=!e&&r.startsWith("_"),p=ee(r);return{isCatchAll:s,isDynamic:a,isGroup:n,isIntercept:e,isOptionalCatchAll:i,isParallel:o,isPrivate:c,segmentName:r,staticKeyName:p}},St=t=>{let e=S.get(t);if(e!==void 0)return e;let r=_.basename(t),n=q(r);if(r==="node_modules"||n.isPrivate)return S.set(t,!1),!1;let o=yt.readdirSync(t,{withFileTypes:!0});for(let i of o){let{name:s}=i,a=_.join(t,s),c=q(s);if(!(s==="node_modules"||c.isPrivate)){if(i.isFile()&&Et.has(s))return S.set(t,!0),!0;if(i.isDirectory()&&St(a))return S.set(t,!0),!0}}return S.set(t,!1),!1},re=(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?Jt:r?Vt:e?zt:""}${o}`}},V=(t,e,r="",n=[])=>{let o=F.get(e);if(o!==void 0)return o;let i=r,s=r+" ",a=[],c=[],p=[],m=[],u=[...n],y=yt.readdirSync(e,{withFileTypes:!0}).filter(h=>{if(h.isDirectory()){let E=_.join(e,h.name);return St(E)}return Et.has(h.name)}).sort((h,E)=>h.name.localeCompare(E.name));for(let h of y){let E=P(_.join(e,h.name));if(h.isDirectory()){let b=h.name,{isGroup:R,isParallel:g,isOptionalCatchAll:l,isCatchAll:f,isDynamic:d,isPrivate:H,isIntercept:Ot,segmentName:jt,staticKeyName:Wt}=q(b);if(H)continue;let{paramName:Yt,keyName:Mt}=re(jt,{isDynamic:d,isCatchAll:f,isOptionalCatchAll:l}),Ut=d||f||l?[...u,{paramName:Yt,routeType:{isDynamic:d,isCatchAll:f,isOptionalCatchAll:l,isGroup:R,isParallel:g}}]:u,ot=R||g,Ht=Ot,{pathStructure:C,imports:Gt,paramsTypes:kt}=V(t,E,ot||g?i:s,Ut);if(m.push(...kt),Ht)continue;if(c.push(...Gt),ot){let w=C.match(/^\s*\{([\s\S]*)\}\s*$/),st=C.trim();if(!C)continue;if(w)a.push(`${s}${w[1].trim()}`);else if(st)p.push(st);else throw new Error(`Invalid empty child path structure in grouped/parallel route: ${E}`)}else{if(!C.trim())continue;let w=d||f||l?Mt:Wt;a.push(`${s}"${w}": ${C}`)}}else{let{query:b,routes:R}=dt(t,E);if(b){let{importStatement:g,importPath:l,type:f}=b;c.push({statement:g,path:l}),p.push(f)}if(R.forEach(g=>{let{importStatement:l,importPath:f,type:d}=g;c.push({statement:l,path:f}),p.push(d)}),p.push(k),u.length>0){let g=u.map(({paramName:f,routeType:d})=>{let H=d.isCatchAll?"string[]":d.isOptionalCatchAll?"string[] | undefined":"string";return{name:f,type:H}}),l=L(g);m.push({paramsType:l,dirPath:_.dirname(E)}),p.push(v(X,l))}}}let M=p.join(" & "),U=a.length>0?`{${`
|
|
3
|
+
`}${a.join(`,${`
|
|
4
4
|
`}`)}${`
|
|
5
|
-
`}${i}}`:"",
|
|
6
|
-
`)}`:"",
|
|
7
|
-
`}${
|
|
5
|
+
`}${i}}`:"",nt={pathStructure:M&&U?`${M} & ${U}`:M||U,imports:c,paramsTypes:m};return F.set(e,nt),nt};var Pt=(t,e)=>{let{pathStructure:r,imports:n,paramsTypes:o}=V(t,e),i=`export type PathStructure = ${r}${";"}`,s=n.length?`${n.sort((m,u)=>m.path.localeCompare(u.path,void 0,{numeric:!0})).map(m=>m.statement).join(`
|
|
6
|
+
`)}`:"",a=ct.filter(m=>r.includes(m)),c=O(a.join(" ,"),pt),p=o.map(({paramsType:m,dirPath:u})=>({paramsType:`export type Params = ${m}${";"}`,dirPath:u}));return{pathStructure:`${c}${`
|
|
7
|
+
`}${s}${`
|
|
8
8
|
`}${`
|
|
9
|
-
`}${i}`,paramsTypes:p}};var
|
|
9
|
+
`}${i}`,paramsTypes:p}};var ne=()=>!!process.stdout?.isTTY;var z=t=>e=>ne()?`\x1B[${t}m${e}\x1B[0m`:e,xt=z(36),$t=z(32),J=z(31);var A=(t,e,r="\u2192",n=24)=>`${t.padEnd(n)} ${r} ${e}`,Z=(t=0)=>" ".repeat(t),Ct=()=>({info:(t,e={})=>{let{indentLevel:r=0,event:n}=e,o=n?`${xt(`[${n}]`)} `:"";console.log(`${Z(r)}${o}${t}`)},success:(t,e={})=>{let{indentLevel:r=0}=e;console.log(`${Z(r)}${$t("\u2713")} ${t}`)},error:(t,e={})=>{let{indentLevel:r=0}=e;console.error(`${Z(r)}${J("\u2717")} ${J(t)}`)}});var It=(t,e)=>tt.existsSync(t)&&tt.readFileSync(t,"utf8")===e?!1:(tt.writeFileSync(t,e),!0),_t=({baseDir:t,outputPath:e,paramsFileName:r,logger:n})=>{n.info("Generating types...",{event:"generate"});let{pathStructure:o,paramsTypes:i}=Pt(e,t);if(It(e,o)?n.success(A("Path structure type",x(e),"\u2192",20),{indentLevel:1}):n.info(A("Unchanged path type",x(e),"\u2192",20),{indentLevel:1}),r){let s=!1;i.forEach(({paramsType:a,dirPath:c})=>{let p=oe.join(c,r),m=It(p,a);s=s||m}),s?n.success(A("Params types",r,"\u2192",20),{indentLevel:1}):n.info(A("Unchanged params",r,"\u2192",20),{indentLevel:1})}};import se from"chokidar";var At=(t,e)=>{let r=null,n=!1,o=null,i=async(...s)=>{n=!0;try{await t(...s)}finally{if(n=!1,o){let a=o;o=null,i(...a)}}};return(...s)=>{r&&clearTimeout(r),r=setTimeout(()=>{if(n){o=s;return}i(...s)},e)}};var Nt=(t,e,r)=>{r.info(`${x(t)}`,{event:"watch"});let n=c=>D.some(p=>c.endsWith(p)),o=new Set,i=At(async()=>{let c=Array.from(o);o.clear();for(let p of c)ut(p),lt(p);await e()},300),s=se.watch(t,{ignoreInitial:!0,ignored:(c,p)=>!!p?.isFile()&&!n(c)});s.on("ready",()=>{i(),s.on("all",(c,p)=>{if(n(p)){let m=x(p);r.info(m,{event:c}),o.add(p),i()}})}),s.on("error",c=>{c instanceof Error?r.error(`Watcher error: ${c.message}`):r.error(`Unknown watcher error: ${String(c)}`)});let a=()=>{s.close().then(()=>{r.info("Watcher closed.",{event:"watch"})}).catch(c=>{r.error(`Failed to close watcher: ${c.message}`)})};process.on("SIGINT",a),process.on("SIGTERM",a)};var Rt=(t,e,r,n)=>{try{return _t({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}},Dt=(t,e,r,n)=>{let o=P(bt.resolve(t)),i=P(bt.resolve(e)),s=typeof r.paramsFile=="string"?r.paramsFile:null;return r.paramsFile!==void 0&&!s?(n.error("Error: --params-file requires a filename."),1):r.watch?(Nt(o,()=>{Rt(o,i,s,n)},n),0):Rt(o,i,s,n)};import Ft from"node:fs";import ie from"node:path";var N="rpc4next.config.json",et=t=>typeof t=="string"&&t.length>0,ae=(t=process.cwd())=>ie.join(t,N),vt=(t=process.cwd())=>{let e=ae(t);if(!Ft.existsSync(e))return{};let r=JSON.parse(Ft.readFileSync(e,"utf8"));if(!r||Array.isArray(r))throw new Error(`${N} must contain a JSON object.`);let n={};if("baseDir"in r){if(!et(r.baseDir))throw new Error(`${N} field "baseDir" must be a string.`);n.baseDir=r.baseDir}if("outputPath"in r){if(!et(r.outputPath))throw new Error(`${N} field "outputPath" must be a string.`);n.outputPath=r.outputPath}if("paramsFile"in r){if(!et(r.paramsFile))throw new Error(`${N} field "paramsFile" must be a string.`);n.paramsFile=r.paramsFile}return n};function me(t){if(t.length===0)return[];if(t[0].startsWith("-"))return t;let e=ce.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 rt(t){let e=`
|
|
10
10
|
Generate RPC client type definitions based on the Next.js path structure.
|
|
11
11
|
|
|
12
12
|
Usage:
|
|
13
13
|
rpc4next <baseDir> <outputPath> [options]
|
|
14
|
+
rpc4next [options]
|
|
14
15
|
|
|
15
16
|
Arguments:
|
|
16
17
|
baseDir Base directory containing Next.js paths for type generation
|
|
17
18
|
outputPath Output path for the generated type definitions
|
|
19
|
+
Both can be omitted when rpc4next.config.json provides them
|
|
18
20
|
|
|
19
21
|
Options:
|
|
20
22
|
-w, --watch Watch mode: regenerate on file changes
|
|
21
23
|
-p, --params-file [filename] Generate params types file (optional filename)
|
|
24
|
+
Config file: rpc4next.config.json in the current directory
|
|
22
25
|
-h, --help Show help
|
|
23
|
-
`.trim();t.info(e)}function
|
|
26
|
+
`.trim();t.info(e)}function ue(t,e){let r=[],n;for(let o=0;o<t.length;o++){let i=t[o],s=!1;for(let a of e)if(i.startsWith(`${a}=`)){n=i.slice(`${a}=`.length)||!0,s=!0;break}if(!s){if(e.includes(i)){let a=t[o+1];typeof a=="string"&&!a.startsWith("-")?(n=a,o++):n=!0;continue}r.push(i)}}return{args:r,value:n}}var Lt=(t,e=Ct())=>{try{let r=me(t),{args:n,value:o}=ue(r,["-p","--params-file"]),{values:i,positionals:s}=pe({args:n,options:{watch:{type:"boolean",short:"w"},help:{type:"boolean",short:"h"}},allowPositionals:!0,strict:!0});i.help&&(rt(e),process.exit(0));let a=vt(),c=s[0]??a.baseDir,p=s[1]??a.outputPath,m=o!==void 0?o:a.paramsFile;(!c||!p)&&(e.error("Missing required arguments: <baseDir> <outputPath>"),rt(e),process.exit(1));let u={watch:!!i.watch,...m!==void 0?{paramsFile:m}:{}};(async()=>{try{let y=await Dt(c,p,u,e);u.watch||process.exit(y)}catch(y){e.error(`Unexpected error occurred:${y instanceof Error?y.message:String(y)}`),process.exit(1)}})()}catch(r){e.error(r instanceof Error?r.message:`Invalid arguments: ${String(r)}`),rt(e),process.exit(1)}};Lt(process.argv);
|
|
24
27
|
/*!
|
|
25
28
|
* Inspired by pathpida (https://github.com/aspida/pathpida),
|
|
26
29
|
* especially the design and UX of its CLI.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rpc4next-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Command line interface for rpc4next. Generates RPC client type definitions from Next.js routes.",
|
|
5
5
|
"homepage": "https://github.com/watanabe-1/rpc4next#readme",
|
|
6
6
|
"repository": {
|