From 11fdf9019c5bd226c510c044405446652b968cc0 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Mon, 8 Apr 2024 08:53:27 +0100 Subject: [PATCH 01/15] chore: npx create-next-app@latest --- .../typescript-nextjs/.gitignore | 36 + integration-tests/typescript-nextjs/README.md | 36 + .../typescript-nextjs/next.config.mjs | 4 + .../typescript-nextjs/package.json | 22 + .../typescript-nextjs/public/next.svg | 1 + .../typescript-nextjs/public/vercel.svg | 1 + .../typescript-nextjs/src/app/favicon.ico | Bin 0 -> 25931 bytes .../typescript-nextjs/src/app/globals.css | 107 +++ .../typescript-nextjs/src/app/layout.tsx | 22 + .../typescript-nextjs/src/app/page.module.css | 230 +++++ .../typescript-nextjs/src/app/page.tsx | 95 +++ .../typescript-nextjs/tsconfig.json | 26 + yarn.lock | 789 +++++++++++++++++- 13 files changed, 1341 insertions(+), 28 deletions(-) create mode 100644 integration-tests/typescript-nextjs/.gitignore create mode 100644 integration-tests/typescript-nextjs/README.md create mode 100644 integration-tests/typescript-nextjs/next.config.mjs create mode 100644 integration-tests/typescript-nextjs/package.json create mode 100644 integration-tests/typescript-nextjs/public/next.svg create mode 100644 integration-tests/typescript-nextjs/public/vercel.svg create mode 100644 integration-tests/typescript-nextjs/src/app/favicon.ico create mode 100644 integration-tests/typescript-nextjs/src/app/globals.css create mode 100644 integration-tests/typescript-nextjs/src/app/layout.tsx create mode 100644 integration-tests/typescript-nextjs/src/app/page.module.css create mode 100644 integration-tests/typescript-nextjs/src/app/page.tsx create mode 100644 integration-tests/typescript-nextjs/tsconfig.json diff --git a/integration-tests/typescript-nextjs/.gitignore b/integration-tests/typescript-nextjs/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/integration-tests/typescript-nextjs/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/integration-tests/typescript-nextjs/README.md b/integration-tests/typescript-nextjs/README.md new file mode 100644 index 000000000..c4033664f --- /dev/null +++ b/integration-tests/typescript-nextjs/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/integration-tests/typescript-nextjs/next.config.mjs b/integration-tests/typescript-nextjs/next.config.mjs new file mode 100644 index 000000000..4678774e6 --- /dev/null +++ b/integration-tests/typescript-nextjs/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/integration-tests/typescript-nextjs/package.json b/integration-tests/typescript-nextjs/package.json new file mode 100644 index 000000000..fb70e3326 --- /dev/null +++ b/integration-tests/typescript-nextjs/package.json @@ -0,0 +1,22 @@ +{ + "name": "typescript-nextjs", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "14.1.4", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "typescript": "^5" + } +} diff --git a/integration-tests/typescript-nextjs/public/next.svg b/integration-tests/typescript-nextjs/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/integration-tests/typescript-nextjs/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration-tests/typescript-nextjs/public/vercel.svg b/integration-tests/typescript-nextjs/public/vercel.svg new file mode 100644 index 000000000..d2f842227 --- /dev/null +++ b/integration-tests/typescript-nextjs/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/integration-tests/typescript-nextjs/src/app/favicon.ico b/integration-tests/typescript-nextjs/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/integration-tests/typescript-nextjs/src/app/globals.css b/integration-tests/typescript-nextjs/src/app/globals.css new file mode 100644 index 000000000..f4bd77c0c --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/globals.css @@ -0,0 +1,107 @@ +:root { + --max-width: 1100px; + --border-radius: 12px; + --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", + "Fira Mono", "Droid Sans Mono", "Courier New", monospace; + + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; + + --primary-glow: conic-gradient( + from 180deg at 50% 50%, + #16abff33 0deg, + #0885ff33 55deg, + #54d6ff33 120deg, + #0071ff33 160deg, + transparent 360deg + ); + --secondary-glow: radial-gradient( + rgba(255, 255, 255, 1), + rgba(255, 255, 255, 0) + ); + + --tile-start-rgb: 239, 245, 249; + --tile-end-rgb: 228, 232, 233; + --tile-border: conic-gradient( + #00000080, + #00000040, + #00000030, + #00000020, + #00000010, + #00000010, + #00000080 + ); + + --callout-rgb: 238, 240, 241; + --callout-border-rgb: 172, 175, 176; + --card-rgb: 180, 185, 188; + --card-border-rgb: 131, 134, 135; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 0, 0, 0; + --background-end-rgb: 0, 0, 0; + + --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); + --secondary-glow: linear-gradient( + to bottom right, + rgba(1, 65, 255, 0), + rgba(1, 65, 255, 0), + rgba(1, 65, 255, 0.3) + ); + + --tile-start-rgb: 2, 13, 46; + --tile-end-rgb: 2, 5, 19; + --tile-border: conic-gradient( + #ffffff80, + #ffffff40, + #ffffff30, + #ffffff20, + #ffffff10, + #ffffff10, + #ffffff80 + ); + + --callout-rgb: 20, 20, 20; + --callout-border-rgb: 108, 108, 108; + --card-rgb: 100, 100, 100; + --card-border-rgb: 200, 200, 200; + } +} + +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + max-width: 100vw; + overflow-x: hidden; +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + +a { + color: inherit; + text-decoration: none; +} + +@media (prefers-color-scheme: dark) { + html { + color-scheme: dark; + } +} diff --git a/integration-tests/typescript-nextjs/src/app/layout.tsx b/integration-tests/typescript-nextjs/src/app/layout.tsx new file mode 100644 index 000000000..3314e4780 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/integration-tests/typescript-nextjs/src/app/page.module.css b/integration-tests/typescript-nextjs/src/app/page.module.css new file mode 100644 index 000000000..5c4b1e6a2 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/page.module.css @@ -0,0 +1,230 @@ +.main { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 6rem; + min-height: 100vh; +} + +.description { + display: inherit; + justify-content: inherit; + align-items: inherit; + font-size: 0.85rem; + max-width: var(--max-width); + width: 100%; + z-index: 2; + font-family: var(--font-mono); +} + +.description a { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; +} + +.description p { + position: relative; + margin: 0; + padding: 1rem; + background-color: rgba(var(--callout-rgb), 0.5); + border: 1px solid rgba(var(--callout-border-rgb), 0.3); + border-radius: var(--border-radius); +} + +.code { + font-weight: 700; + font-family: var(--font-mono); +} + +.grid { + display: grid; + grid-template-columns: repeat(4, minmax(25%, auto)); + max-width: 100%; + width: var(--max-width); +} + +.card { + padding: 1rem 1.2rem; + border-radius: var(--border-radius); + background: rgba(var(--card-rgb), 0); + border: 1px solid rgba(var(--card-border-rgb), 0); + transition: background 200ms, border 200ms; +} + +.card span { + display: inline-block; + transition: transform 200ms; +} + +.card h2 { + font-weight: 600; + margin-bottom: 0.7rem; +} + +.card p { + margin: 0; + opacity: 0.6; + font-size: 0.9rem; + line-height: 1.5; + max-width: 30ch; + text-wrap: balance; +} + +.center { + display: flex; + justify-content: center; + align-items: center; + position: relative; + padding: 4rem 0; +} + +.center::before { + background: var(--secondary-glow); + border-radius: 50%; + width: 480px; + height: 360px; + margin-left: -400px; +} + +.center::after { + background: var(--primary-glow); + width: 240px; + height: 180px; + z-index: -1; +} + +.center::before, +.center::after { + content: ""; + left: 50%; + position: absolute; + filter: blur(45px); + transform: translateZ(0); +} + +.logo { + position: relative; +} +/* Enable hover only on non-touch devices */ +@media (hover: hover) and (pointer: fine) { + .card:hover { + background: rgba(var(--card-rgb), 0.1); + border: 1px solid rgba(var(--card-border-rgb), 0.15); + } + + .card:hover span { + transform: translateX(4px); + } +} + +@media (prefers-reduced-motion) { + .card:hover span { + transform: none; + } +} + +/* Mobile */ +@media (max-width: 700px) { + .content { + padding: 4rem; + } + + .grid { + grid-template-columns: 1fr; + margin-bottom: 120px; + max-width: 320px; + text-align: center; + } + + .card { + padding: 1rem 2.5rem; + } + + .card h2 { + margin-bottom: 0.5rem; + } + + .center { + padding: 8rem 0 6rem; + } + + .center::before { + transform: none; + height: 300px; + } + + .description { + font-size: 0.8rem; + } + + .description a { + padding: 1rem; + } + + .description p, + .description div { + display: flex; + justify-content: center; + position: fixed; + width: 100%; + } + + .description p { + align-items: center; + inset: 0 0 auto; + padding: 2rem 1rem 1.4rem; + border-radius: 0; + border: none; + border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); + background: linear-gradient( + to bottom, + rgba(var(--background-start-rgb), 1), + rgba(var(--callout-rgb), 0.5) + ); + background-clip: padding-box; + backdrop-filter: blur(24px); + } + + .description div { + align-items: flex-end; + pointer-events: none; + inset: auto 0 0; + padding: 2rem; + height: 200px; + background: linear-gradient( + to bottom, + transparent 0%, + rgb(var(--background-end-rgb)) 40% + ); + z-index: 1; + } +} + +/* Tablet and Smaller Desktop */ +@media (min-width: 701px) and (max-width: 1120px) { + .grid { + grid-template-columns: repeat(2, 50%); + } +} + +@media (prefers-color-scheme: dark) { + .vercelLogo { + filter: invert(1); + } + + .logo { + filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); + } +} + +@keyframes rotate { + from { + transform: rotate(360deg); + } + to { + transform: rotate(0deg); + } +} diff --git a/integration-tests/typescript-nextjs/src/app/page.tsx b/integration-tests/typescript-nextjs/src/app/page.tsx new file mode 100644 index 000000000..d2c63a496 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/page.tsx @@ -0,0 +1,95 @@ +import Image from "next/image"; +import styles from "./page.module.css"; + +export default function Home() { + return ( +
+
+

+ Get started by editing  + src/app/page.tsx +

+ +
+ +
+ Next.js Logo +
+ + +
+ ); +} diff --git a/integration-tests/typescript-nextjs/tsconfig.json b/integration-tests/typescript-nextjs/tsconfig.json new file mode 100644 index 000000000..7b2858930 --- /dev/null +++ b/integration-tests/typescript-nextjs/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock index 3d8582f7e..0996276b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1911,7 +1911,17 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.27.7, @babel/types@npm:^7.28.0, @babel/types@npm:^7.4.4": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.4.4": + version: 7.27.3 + resolution: "@babel/types@npm:7.27.3" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10/a24e6accd85c4747b974b3d68a3210d0aa1180c1a77b287ffcb7401cd2edad7bdecadaeb40fe5191be3990c3a5252943f7de7c09da13ed269adbb054b97056ee + languageName: node + linkType: hard + +"@babel/types@npm:^7.27.6, @babel/types@npm:^7.27.7, @babel/types@npm:^7.28.0": version: 7.28.0 resolution: "@babel/types@npm:7.28.0" dependencies: @@ -2144,6 +2154,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/aix-ppc64@npm:0.25.3" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/aix-ppc64@npm:0.25.5" @@ -2151,6 +2168,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/android-arm64@npm:0.25.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm64@npm:0.25.5" @@ -2158,6 +2182,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/android-arm@npm:0.25.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm@npm:0.25.5" @@ -2165,6 +2196,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/android-x64@npm:0.25.3" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-x64@npm:0.25.5" @@ -2172,6 +2210,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/darwin-arm64@npm:0.25.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-arm64@npm:0.25.5" @@ -2179,6 +2224,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/darwin-x64@npm:0.25.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-x64@npm:0.25.5" @@ -2186,6 +2238,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/freebsd-arm64@npm:0.25.3" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-arm64@npm:0.25.5" @@ -2193,6 +2252,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/freebsd-x64@npm:0.25.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-x64@npm:0.25.5" @@ -2200,6 +2266,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-arm64@npm:0.25.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm64@npm:0.25.5" @@ -2207,6 +2280,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-arm@npm:0.25.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm@npm:0.25.5" @@ -2214,6 +2294,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-ia32@npm:0.25.3" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ia32@npm:0.25.5" @@ -2221,6 +2308,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-loong64@npm:0.25.3" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-loong64@npm:0.25.5" @@ -2228,6 +2322,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-mips64el@npm:0.25.3" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-mips64el@npm:0.25.5" @@ -2235,6 +2336,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-ppc64@npm:0.25.3" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ppc64@npm:0.25.5" @@ -2242,6 +2350,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-riscv64@npm:0.25.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-riscv64@npm:0.25.5" @@ -2249,6 +2364,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-s390x@npm:0.25.3" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-s390x@npm:0.25.5" @@ -2256,6 +2378,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/linux-x64@npm:0.25.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-x64@npm:0.25.5" @@ -2263,6 +2392,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/netbsd-arm64@npm:0.25.3" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-arm64@npm:0.25.5" @@ -2270,6 +2406,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/netbsd-x64@npm:0.25.3" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-x64@npm:0.25.5" @@ -2277,6 +2420,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/openbsd-arm64@npm:0.25.3" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-arm64@npm:0.25.5" @@ -2284,6 +2434,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/openbsd-x64@npm:0.25.3" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-x64@npm:0.25.5" @@ -2291,6 +2448,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/sunos-x64@npm:0.25.3" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/sunos-x64@npm:0.25.5" @@ -2298,6 +2462,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/win32-arm64@npm:0.25.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-arm64@npm:0.25.5" @@ -2305,6 +2476,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/win32-ia32@npm:0.25.3" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-ia32@npm:0.25.5" @@ -2312,6 +2490,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.25.3": + version: 0.25.3 + resolution: "@esbuild/win32-x64@npm:0.25.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-x64@npm:0.25.5" @@ -2364,13 +2549,20 @@ __metadata: languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.10, @floating-ui/utils@npm:^0.2.8": +"@floating-ui/utils@npm:^0.2.10": version: 0.2.10 resolution: "@floating-ui/utils@npm:0.2.10" checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae languageName: node linkType: hard +"@floating-ui/utils@npm:^0.2.8": + version: 0.2.9 + resolution: "@floating-ui/utils@npm:0.2.9" + checksum: 10/0ca786347db3dd8d9034b86d1449fabb96642788e5900cc5f2aee433cd7b243efbcd7a165bead50b004ee3f20a90ddebb6a35296fc41d43cfd361b6f01b69ffb + languageName: node + linkType: hard + "@formatjs/intl-localematcher@npm:^0.6.0": version: 0.6.1 resolution: "@formatjs/intl-localematcher@npm:0.6.1" @@ -3269,7 +3461,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": +"@jridgewell/gen-mapping@npm:^0.3.12": version: 0.3.12 resolution: "@jridgewell/gen-mapping@npm:0.3.12" dependencies: @@ -3279,6 +3471,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.8 + resolution: "@jridgewell/gen-mapping@npm:0.3.8" + dependencies: + "@jridgewell/set-array": "npm:^1.2.1" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/9d3a56ab3612ab9b85d38b2a93b87f3324f11c5130859957f6500e4ac8ce35f299d5ccc3ecd1ae87597601ecf83cee29e9afd04c18777c24011073992ff946df + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" @@ -3286,6 +3489,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.3": version: 0.3.10 resolution: "@jridgewell/source-map@npm:0.3.10" @@ -3296,6 +3506,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.4.10": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd + languageName: node + linkType: hard + "@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.4 resolution: "@jridgewell/sourcemap-codec@npm:1.5.4" @@ -4136,6 +4353,13 @@ __metadata: languageName: node linkType: hard +"@next/env@npm:14.1.4": + version: 14.1.4 + resolution: "@next/env@npm:14.1.4" + checksum: 10/76db04d141aed6e4e7f64619f66b84b39a01fd698db735381b530347794b252d74f9d71fe6787402f986a5202e9a4ce1d9c2569fec7c56e67e346c0522883b8b + languageName: node + linkType: hard + "@next/env@npm:15.3.5": version: 15.3.5 resolution: "@next/env@npm:15.3.5" @@ -4143,6 +4367,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-arm64@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-darwin-arm64@npm:14.1.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-darwin-arm64@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-darwin-arm64@npm:15.3.5" @@ -4150,6 +4381,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-darwin-x64@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-darwin-x64@npm:14.1.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@next/swc-darwin-x64@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-darwin-x64@npm:15.3.5" @@ -4157,6 +4395,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-gnu@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-linux-arm64-gnu@npm:14.1.4" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-arm64-gnu@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-arm64-gnu@npm:15.3.5" @@ -4164,6 +4409,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-arm64-musl@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-linux-arm64-musl@npm:14.1.4" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@next/swc-linux-arm64-musl@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-arm64-musl@npm:15.3.5" @@ -4171,6 +4423,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-gnu@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-linux-x64-gnu@npm:14.1.4" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@next/swc-linux-x64-gnu@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-x64-gnu@npm:15.3.5" @@ -4178,6 +4437,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-linux-x64-musl@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-linux-x64-musl@npm:14.1.4" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@next/swc-linux-x64-musl@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-x64-musl@npm:15.3.5" @@ -4185,6 +4451,13 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-arm64-msvc@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-win32-arm64-msvc@npm:14.1.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-win32-arm64-msvc@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-win32-arm64-msvc@npm:15.3.5" @@ -4192,6 +4465,20 @@ __metadata: languageName: node linkType: hard +"@next/swc-win32-ia32-msvc@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-win32-ia32-msvc@npm:14.1.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@next/swc-win32-x64-msvc@npm:14.1.4": + version: 14.1.4 + resolution: "@next/swc-win32-x64-msvc@npm:14.1.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@next/swc-win32-x64-msvc@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-win32-x64-msvc@npm:15.3.5" @@ -5722,6 +6009,15 @@ __metadata: languageName: node linkType: hard +"@swc/helpers@npm:0.5.2": + version: 0.5.2 + resolution: "@swc/helpers@npm:0.5.2" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/3a3b179b3369acd26c5da89a0e779c756ae5231eb18a5507524c7abf955f488d34d86649f5b8417a0e19879688470d06319f5cfca2273d6d6b2046950e0d79af + languageName: node + linkType: hard + "@swc/helpers@npm:^0.5.0": version: 0.5.17 resolution: "@swc/helpers@npm:0.5.17" @@ -6380,7 +6676,7 @@ __metadata: languageName: node linkType: hard -"@types/http-proxy@npm:^1.17.15, @types/http-proxy@npm:^1.17.8": +"@types/http-proxy@npm:^1.17.15": version: 1.17.16 resolution: "@types/http-proxy@npm:1.17.16" dependencies: @@ -6389,6 +6685,15 @@ __metadata: languageName: node linkType: hard +"@types/http-proxy@npm:^1.17.8": + version: 1.17.14 + resolution: "@types/http-proxy@npm:1.17.14" + dependencies: + "@types/node": "npm:*" + checksum: 10/aa1a3e66cd43cbf06ea5901bf761d2031200a0ab42ba7e462a15c752e70f8669f21fb3be7c2f18fefcb83b95132dfa15740282e7421b856745598fbaea8e3a42 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6570,6 +6875,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20": + version: 20.12.5 + resolution: "@types/node@npm:20.12.5" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10/7b647ea6679016e4e58e1aa439c46b610230ffcbe19173911fbf1d1fa329ec6fd1eeba4e3e2d8743206d3b00d5a0cad75f1c90189e1d1ec057eb48df1a1dd747 + languageName: node + linkType: hard + "@types/node@npm:^22.16.0": version: 22.16.0 resolution: "@types/node@npm:22.16.0" @@ -6595,6 +6909,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.13 + resolution: "@types/prop-types@npm:15.7.13" + checksum: 10/8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c + languageName: node + linkType: hard + "@types/qs@npm:*, @types/qs@npm:^6.14.0": version: 6.14.0 resolution: "@types/qs@npm:6.14.0" @@ -6609,6 +6930,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18": + version: 18.2.24 + resolution: "@types/react-dom@npm:18.2.24" + dependencies: + "@types/react": "npm:*" + checksum: 10/bbd4005f2f65b7606505e9b8759b6e99e222d503602765594ea327893fb7061de8951279baef47a1932f04d94d1865daea05a32f9fcf6f9f1143dbabce5b33de + languageName: node + linkType: hard + "@types/react-dom@npm:^19.1.6": version: 19.1.6 resolution: "@types/react-dom@npm:19.1.6" @@ -6618,6 +6948,16 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:*, @types/react@npm:^18": + version: 18.2.74 + resolution: "@types/react@npm:18.2.74" + dependencies: + "@types/prop-types": "npm:*" + csstype: "npm:^3.0.2" + checksum: 10/4057aa7d082d434f8e580e5aebd4007e5dbe7f8e9ae5e506a34a629e382070694a0401bf3f0d38fe8d64f4b38622e5794341e634b9739784deae19b037ae43fa + languageName: node + linkType: hard + "@types/react@npm:^19.1.8": version: 19.1.8 resolution: "@types/react@npm:19.1.8" @@ -7258,7 +7598,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.14.0, acorn@npm:^8.8.2": +"acorn@npm:^8.0.0, acorn@npm:^8.14.0": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -7267,6 +7607,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.8.2": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" + bin: + acorn: bin/acorn + checksum: 10/b688e7e3c64d9bfb17b596e1b35e4da9d50553713b3b3630cf5690f2b023a84eac90c56851e6912b483fe60e8b4ea28b254c07e92f17ef83d72d78745a8352dd + languageName: node + linkType: hard + "add-stream@npm:^1.0.0": version: 1.0.0 resolution: "add-stream@npm:1.0.0" @@ -7651,7 +8000,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.10.0, axios@npm:^1.8.3": +"axios@npm:^1.10.0": version: 1.10.0 resolution: "axios@npm:1.10.0" dependencies: @@ -7662,6 +8011,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.8.3": + version: 1.9.0 + resolution: "axios@npm:1.9.0" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/a2f90bba56820883879f32a237e2b9ff25c250365dcafd41cec41b3406a3df334a148f90010182dfdadb4b41dc59f6f0b3e8898ff41b666d1157b5f3f4523497 + languageName: node + linkType: hard + "babel-jest@npm:30.0.4": version: 30.0.4 resolution: "babel-jest@npm:30.0.4" @@ -7981,7 +8341,16 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:^3.0.3, braces@npm:~3.0.2": +"braces@npm:^3.0.2, braces@npm:~3.0.2": + version: 3.0.2 + resolution: "braces@npm:3.0.2" + dependencies: + fill-range: "npm:^7.0.1" + checksum: 10/966b1fb48d193b9d155f810e5efd1790962f2c4e0829f8440b8ad236ba009222c501f70185ef732fef17a4c490bb33a03b90dab0631feafbdf447da91e8165b1 + languageName: node + linkType: hard + +"braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -8217,7 +8586,7 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: @@ -8227,6 +8596,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1": + version: 1.0.1 + resolution: "call-bind-apply-helpers@npm:1.0.1" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10/6e30c621170e45f1fd6735e84d02ee8e02a3ab95cb109499d5308cbe5d1e84d0cd0e10b48cc43c76aa61450ae1b03a7f89c37c10fc0de8d4998b42aab0f268cc + languageName: node + linkType: hard + "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" @@ -9773,27 +10152,39 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a languageName: node linkType: hard -"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" +"debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a + checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + languageName: node + linkType: hard + +"debug@npm:^4.4.1": + version: 4.4.1 + resolution: "debug@npm:4.4.1" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe languageName: node linkType: hard @@ -10198,7 +10589,7 @@ __metadata: languageName: node linkType: hard -"dunder-proto@npm:^1.0.1": +"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" dependencies: @@ -10554,7 +10945,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.25.5, esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": +"esbuild@npm:0.25.5": version: 0.25.5 resolution: "esbuild@npm:0.25.5" dependencies: @@ -10640,6 +11031,92 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": + version: 0.25.3 + resolution: "esbuild@npm:0.25.3" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.3" + "@esbuild/android-arm": "npm:0.25.3" + "@esbuild/android-arm64": "npm:0.25.3" + "@esbuild/android-x64": "npm:0.25.3" + "@esbuild/darwin-arm64": "npm:0.25.3" + "@esbuild/darwin-x64": "npm:0.25.3" + "@esbuild/freebsd-arm64": "npm:0.25.3" + "@esbuild/freebsd-x64": "npm:0.25.3" + "@esbuild/linux-arm": "npm:0.25.3" + "@esbuild/linux-arm64": "npm:0.25.3" + "@esbuild/linux-ia32": "npm:0.25.3" + "@esbuild/linux-loong64": "npm:0.25.3" + "@esbuild/linux-mips64el": "npm:0.25.3" + "@esbuild/linux-ppc64": "npm:0.25.3" + "@esbuild/linux-riscv64": "npm:0.25.3" + "@esbuild/linux-s390x": "npm:0.25.3" + "@esbuild/linux-x64": "npm:0.25.3" + "@esbuild/netbsd-arm64": "npm:0.25.3" + "@esbuild/netbsd-x64": "npm:0.25.3" + "@esbuild/openbsd-arm64": "npm:0.25.3" + "@esbuild/openbsd-x64": "npm:0.25.3" + "@esbuild/sunos-x64": "npm:0.25.3" + "@esbuild/win32-arm64": "npm:0.25.3" + "@esbuild/win32-ia32": "npm:0.25.3" + "@esbuild/win32-x64": "npm:0.25.3" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10/f1ff72289938330312926421f90eea442025cbbac295a7a2e8cfc2abbd9e3a8bc1502883468b0487e4020f1369e4726c851a2fa4b65a7c71331940072c3a1808 + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -11121,7 +11598,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.3, fdir@npm:^6.4.4": +"fdir@npm:^6.4.3": version: 6.4.6 resolution: "fdir@npm:6.4.6" peerDependencies: @@ -11133,6 +11610,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.4": + version: 6.4.5 + resolution: "fdir@npm:6.4.5" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/8f5a2107fe0486f61af9a0666f2b7c62a229c738330e22ff8795bfbaabcf2294fb79460b73830b8824fc6eef91e21f676bac66ca982d5ee7e92ee9b68c07775f + languageName: node + linkType: hard + "figures@npm:3.2.0, figures@npm:^3.0.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -11182,6 +11671,15 @@ __metadata: languageName: node linkType: hard +"fill-range@npm:^7.0.1": + version: 7.0.1 + resolution: "fill-range@npm:7.0.1" + dependencies: + to-regex-range: "npm:^5.0.1" + checksum: 10/e260f7592fd196b4421504d3597cc76f4a1ca7a9488260d533b611fc3cefd61e9a9be1417cb82d3b01ad9f9c0ff2dbf258e1026d2445e26b0cf5148ff4250429 + languageName: node + linkType: hard + "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -11497,7 +11995,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -11515,6 +12013,24 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": + version: 1.2.6 + resolution: "get-intrinsic@npm:1.2.6" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + dunder-proto: "npm:^1.0.0" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + function-bind: "npm:^1.1.2" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.0.0" + checksum: 10/a1ffae6d7893a6fa0f4d1472adbc85095edd6b3b0943ead97c3738539cecb19d422ff4d48009eed8c3c27ad678c2b1e38a83b1a1e96b691d13ed8ecefca1068d + languageName: node + linkType: hard + "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -13164,7 +13680,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:6.0.3, istanbul-lib-instrument@npm:^6.0.0, istanbul-lib-instrument@npm:^6.0.2": +"istanbul-lib-instrument@npm:6.0.3, istanbul-lib-instrument@npm:^6.0.2": version: 6.0.3 resolution: "istanbul-lib-instrument@npm:6.0.3" dependencies: @@ -13190,6 +13706,19 @@ __metadata: languageName: node linkType: hard +"istanbul-lib-instrument@npm:^6.0.0": + version: 6.0.2 + resolution: "istanbul-lib-instrument@npm:6.0.2" + dependencies: + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-coverage: "npm:^3.2.0" + semver: "npm:^7.5.4" + checksum: 10/3aee19be199350182827679a137e1df142a306e9d7e20bb5badfd92ecc9023a7d366bc68e7c66e36983654a02a67401d75d8debf29fc6d4b83670fde69a594fc + languageName: node + linkType: hard + "istanbul-lib-report@npm:^3.0.0": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" @@ -13764,7 +14293,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^4.0.0": +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 10/af37d0d913fb56aec6dc0074c163cc71cd23c0b8aad5c2350747b6721d37ba118af35abdd8b33c47ec2800de07dedb16a527ca9c530ee004093e04958bd0cbf2 @@ -14032,7 +14561,18 @@ __metadata: languageName: node linkType: hard -"katex@npm:^0.16.0, katex@npm:^0.16.21, katex@npm:^0.16.9": +"katex@npm:^0.16.0, katex@npm:^0.16.9": + version: 0.16.18 + resolution: "katex@npm:0.16.18" + dependencies: + commander: "npm:^8.3.0" + bin: + katex: cli.js + checksum: 10/5c90a770969eaaa18cb62e5a2f4937704e4378e6c5848e02138b3a681bc4bda5faf3fba33a12642f042345c429fafb4af48c2e36ecbe953f7db2cacb7cedc28f + languageName: node + linkType: hard + +"katex@npm:^0.16.21": version: 0.16.22 resolution: "katex@npm:0.16.22" dependencies: @@ -14664,6 +15204,17 @@ __metadata: languageName: node linkType: hard +"loose-envify@npm:^1.1.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: "npm:^3.0.0 || ^4.0.0" + bin: + loose-envify: cli.js + checksum: 10/6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.2.2": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -14840,7 +15391,7 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.1.0": +"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd @@ -16303,6 +16854,61 @@ __metadata: languageName: node linkType: hard +"next@npm:14.1.4": + version: 14.1.4 + resolution: "next@npm:14.1.4" + dependencies: + "@next/env": "npm:14.1.4" + "@next/swc-darwin-arm64": "npm:14.1.4" + "@next/swc-darwin-x64": "npm:14.1.4" + "@next/swc-linux-arm64-gnu": "npm:14.1.4" + "@next/swc-linux-arm64-musl": "npm:14.1.4" + "@next/swc-linux-x64-gnu": "npm:14.1.4" + "@next/swc-linux-x64-musl": "npm:14.1.4" + "@next/swc-win32-arm64-msvc": "npm:14.1.4" + "@next/swc-win32-ia32-msvc": "npm:14.1.4" + "@next/swc-win32-x64-msvc": "npm:14.1.4" + "@swc/helpers": "npm:0.5.2" + busboy: "npm:1.6.0" + caniuse-lite: "npm:^1.0.30001579" + graceful-fs: "npm:^4.2.11" + postcss: "npm:8.4.31" + styled-jsx: "npm:5.1.1" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + react: ^18.2.0 + react-dom: ^18.2.0 + sass: ^1.3.0 + dependenciesMeta: + "@next/swc-darwin-arm64": + optional: true + "@next/swc-darwin-x64": + optional: true + "@next/swc-linux-arm64-gnu": + optional: true + "@next/swc-linux-arm64-musl": + optional: true + "@next/swc-linux-x64-gnu": + optional: true + "@next/swc-linux-x64-musl": + optional: true + "@next/swc-win32-arm64-msvc": + optional: true + "@next/swc-win32-ia32-msvc": + optional: true + "@next/swc-win32-x64-msvc": + optional: true + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + sass: + optional: true + bin: + next: dist/bin/next + checksum: 10/16dd0667d55caf0b9915c530e4ae950ae7fad42c22573f333cd23f2fee8243afa4d3e8093a1c7d07251ced150c0bed9cde273cac951b919594a8e2112d669266 + languageName: node + linkType: hard + "next@npm:15.3.5": version: 15.3.5 resolution: "next@npm:15.3.5" @@ -16737,7 +17343,7 @@ __metadata: languageName: node linkType: hard -"npm-package-arg@npm:12.0.2, npm-package-arg@npm:^12.0.0": +"npm-package-arg@npm:12.0.2": version: 12.0.2 resolution: "npm-package-arg@npm:12.0.2" dependencies: @@ -16761,6 +17367,18 @@ __metadata: languageName: node linkType: hard +"npm-package-arg@npm:^12.0.0": + version: 12.0.1 + resolution: "npm-package-arg@npm:12.0.1" + dependencies: + hosted-git-info: "npm:^8.0.0" + proc-log: "npm:^5.0.0" + semver: "npm:^7.3.5" + validate-npm-package-name: "npm:^6.0.0" + checksum: 10/63ef48708653aa1a5e4353116cbbe78be3630dcd92a12a559cf9c9a8b71bbaf5016b8c99ab020730587772cc1892fa924bb2ac3baef1c740847b130c56ef531d + languageName: node + linkType: hard + "npm-packlist@npm:8.0.2, npm-packlist@npm:^8.0.0": version: 8.0.2 resolution: "npm-packlist@npm:8.0.2" @@ -18220,7 +18838,16 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.11.0, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.5.2": +"qs@npm:^6.11.0, qs@npm:^6.5.2": + version: 6.13.1 + resolution: "qs@npm:6.13.1" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10/53cf5fdc5f342a9ffd3968f20c8c61624924cf928d86fff525240620faba8ca5cfd6c3f12718cc755561bfc3dc9721bc8924e38f53d8925b03940f0b8a902212 + languageName: node + linkType: hard + +"qs@npm:^6.12.3, qs@npm:^6.14.0": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -18338,6 +18965,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^18": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.0" + peerDependencies: + react: ^18.2.0 + checksum: 10/ca5e7762ec8c17a472a3605b6f111895c9f87ac7d43a610ab7024f68cd833d08eda0625ce02ec7178cc1f3c957cf0b9273cdc17aa2cd02da87544331c43b1d21 + languageName: node + linkType: hard + "react-hook-form@npm:^7.60.0": version: 7.60.0 resolution: "react-hook-form@npm:7.60.0" @@ -18371,6 +19010,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^18": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/b9214a9bd79e99d08de55f8bef2b7fc8c39630be97c4e29d7be173d14a9a10670b5325e94485f74cd8bff4966ef3c78ee53c79a7b0b9b70cba20aa8973acc694 + languageName: node + linkType: hard + "read-cmd-shim@npm:4.0.0, read-cmd-shim@npm:^4.0.0": version: 4.0.0 resolution: "read-cmd-shim@npm:4.0.0" @@ -19368,6 +20016,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10/0c4557aa37bafca44ff21dc0ea7c92e2dbcb298bc62eae92b29a39b029134f02fb23917d6ebc8b1fa536b4184934314c20d8864d156a9f6357f3398aaf7bfda8 + languageName: node + linkType: hard + "scheduler@npm:^0.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" @@ -19590,7 +20247,19 @@ __metadata: languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8": +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": + version: 2.4.11 + resolution: "sha.js@npm:2.4.11" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + bin: + sha.js: ./bin.js + checksum: 10/d833bfa3e0a67579a6ce6e1bc95571f05246e0a441dd8c76e3057972f2a3e098465687a4369b07e83a0375a88703577f71b5b2e966809e67ebc340dbedb478c7 + languageName: node + linkType: hard + +"sha.js@npm:^2.4.11": version: 2.4.12 resolution: "sha.js@npm:2.4.12" dependencies: @@ -20384,6 +21053,22 @@ __metadata: languageName: node linkType: hard +"styled-jsx@npm:5.1.1": + version: 5.1.1 + resolution: "styled-jsx@npm:5.1.1" + dependencies: + client-only: "npm:0.0.1" + peerDependencies: + react: ">= 16.8.0 || 17.x.x || ^18.0.0-0" + peerDependenciesMeta: + "@babel/core": + optional: true + babel-plugin-macros: + optional: true + checksum: 10/4f6a5d0010770fdeea1183d919d528fd46c484e23c0535ef3e1dd49488116f639c594f3bd4440e3bc8a8686c9f8d53c5761599870ff039ede11a5c3bfe08a4be + languageName: node + linkType: hard + "styled-jsx@npm:5.1.6": version: 5.1.6 resolution: "styled-jsx@npm:5.1.6" @@ -20832,13 +21517,20 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2.8.1, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 languageName: node linkType: hard +"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 + languageName: node + linkType: hard + "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -21085,6 +21777,20 @@ __metadata: languageName: unknown linkType: soft +"typescript-nextjs@workspace:integration-tests/typescript-nextjs": + version: 0.0.0-use.local + resolution: "typescript-nextjs@workspace:integration-tests/typescript-nextjs" + dependencies: + "@types/node": "npm:^20" + "@types/react": "npm:^18" + "@types/react-dom": "npm:^18" + next: "npm:14.1.4" + react: "npm:^18" + react-dom: "npm:^18" + typescript: "npm:^5" + languageName: unknown + linkType: soft + "typescript@npm:>=3 < 6, typescript@npm:^5.8.3, typescript@npm:~5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" @@ -21095,6 +21801,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5": + version: 5.4.4 + resolution: "typescript@npm:5.4.4" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/bade322d88fd93c8179e262aca9ba7f7b4417c09117879819c87946578c782ab123e3acb4733046a6e38714c47ef927360045a1f9292a1bff3a05a6577d27ca2 + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A>=3 < 6#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin, typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" @@ -21105,6 +21821,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5#optional!builtin": + version: 5.4.4 + resolution: "typescript@patch:typescript@npm%3A5.4.4#optional!builtin::version=5.4.4&hash=5adc0c" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/88aff3244c31d4c6ede05b4fd28732fc8935a7fc638f2a3dcbbb767d1ac98e4b077f21ec74bc97f43c9307bc3f27e2359def1d793f9918c3429a744408fd75b4 + languageName: node + linkType: hard + "ua-parser-js@npm:^0.7.30": version: 0.7.40 resolution: "ua-parser-js@npm:0.7.40" @@ -21130,6 +21856,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd + languageName: node + linkType: hard + "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" From 56bc96a1350bc4f44f3eb77ad2694484ad3b4909 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Mon, 8 Apr 2024 08:55:07 +0100 Subject: [PATCH 02/15] chore: add scripts --- integration-tests/typescript-nextjs/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/integration-tests/typescript-nextjs/package.json b/integration-tests/typescript-nextjs/package.json index fb70e3326..cd3406a6b 100644 --- a/integration-tests/typescript-nextjs/package.json +++ b/integration-tests/typescript-nextjs/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "clean": "rm -rf ./.next && rm -rf ./src/api", + "validate": "tsc -p ./tsconfig.json" }, "dependencies": { "next": "14.1.4", From 2f4dd69a9c1b54112890d9a9c3b26d8ef79febf2 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Mon, 8 Apr 2024 08:55:42 +0100 Subject: [PATCH 03/15] feat: add typescript-nextjs --- biome.json | 1 + .../typescript-nextjs/next.config.mjs | 4 +- .../typescript-nextjs/package.json | 16 +- .../typescript-nextjs/src/app/globals.css | 3 +- .../typescript-nextjs/src/app/layout.tsx | 14 +- .../typescript-nextjs/src/app/page.tsx | 6 +- .../app/todo-lists.yaml/attachments/route.ts | 13 + .../list/[listId]/items/route.ts | 13 + .../todo-lists.yaml/list/[listId]/route.ts | 18 + .../src/app/todo-lists.yaml/list/route.ts | 6 + .../todo-lists.yaml/attachments/route.ts | 111 ++++ .../todo-lists.yaml/clients/client.ts | 317 +++++++++ .../list/[listId]/items/route.ts | 157 +++++ .../todo-lists.yaml/list/[listId]/route.ts | 211 ++++++ .../generated/todo-lists.yaml/list/route.ts | 82 +++ .../src/generated/todo-lists.yaml/models.ts | 67 ++ .../src/generated/todo-lists.yaml/schemas.ts | 25 + packages/documentation/package.json | 6 +- .../src/lib/playground/load-runtime-types.tsx | 26 +- packages/openapi-code-generator/package.json | 1 + packages/openapi-code-generator/src/config.ts | 1 + .../openapi-code-generator/src/core/input.ts | 14 +- .../src/core/loaders/tsconfig.loader.ts | 2 +- .../src/core/openapi-loader.ts | 2 +- .../src/core/schemas/tsconfig.schema.ts | 1 + .../openapi-code-generator/src/core/utils.ts | 4 + .../openapi-code-generator/src/templates.ts | 7 + .../typescript/common/compilation-units.ts | 3 +- .../src/typescript/common/import-builder.ts | 18 +- .../typescript/common/typescript-emitter.ts | 2 +- .../typescript-nextjs.generator.ts | 613 ++++++++++++++++++ scripts/generate.mjs | 32 +- yarn.lock | 325 ++++------ 33 files changed, 1875 insertions(+), 246 deletions(-) create mode 100644 integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts create mode 100644 integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts create mode 100644 integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts create mode 100644 integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts create mode 100644 integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts create mode 100644 packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts diff --git a/biome.json b/biome.json index af5f0569e..156ec0033 100644 --- a/biome.json +++ b/biome.json @@ -42,6 +42,7 @@ "packages/openapi-code-generator/src/core/schemas/openapi-3.0-specification-validator.js", "packages/openapi-code-generator/src/core/schemas/openapi-3.1-specification-validator.js", "integration-tests/**/generated/*", + "integration-tests/typescript-nextjs/src/app/**/*", "integration-tests-definitions/**/*", "e2e/src/generated/**/*", "schemas/*.json", diff --git a/integration-tests/typescript-nextjs/next.config.mjs b/integration-tests/typescript-nextjs/next.config.mjs index 4678774e6..1d6147825 100644 --- a/integration-tests/typescript-nextjs/next.config.mjs +++ b/integration-tests/typescript-nextjs/next.config.mjs @@ -1,4 +1,4 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = {} -export default nextConfig; +export default nextConfig diff --git a/integration-tests/typescript-nextjs/package.json b/integration-tests/typescript-nextjs/package.json index cd3406a6b..4afa64d1a 100644 --- a/integration-tests/typescript-nextjs/package.json +++ b/integration-tests/typescript-nextjs/package.json @@ -7,18 +7,18 @@ "build": "next build", "start": "next start", "lint": "next lint", - "clean": "rm -rf ./.next && rm -rf ./src/api", + "clean": "rm -rf ./.next && rm -rf ./src/generated && rm -rf -- ./src/app/*/", "validate": "tsc -p ./tsconfig.json" }, "dependencies": { - "next": "14.1.4", - "react": "^18", - "react-dom": "^18" + "next": "^15.3.1", + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "typescript": "^5" + "@types/node": "^22.15.3", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.3", + "typescript": "^5.8.3" } } diff --git a/integration-tests/typescript-nextjs/src/app/globals.css b/integration-tests/typescript-nextjs/src/app/globals.css index f4bd77c0c..acf750815 100644 --- a/integration-tests/typescript-nextjs/src/app/globals.css +++ b/integration-tests/typescript-nextjs/src/app/globals.css @@ -1,7 +1,8 @@ :root { --max-width: 1100px; --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", + --font-mono: + ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", "Fira Mono", "Droid Sans Mono", "Courier New", monospace; diff --git a/integration-tests/typescript-nextjs/src/app/layout.tsx b/integration-tests/typescript-nextjs/src/app/layout.tsx index 3314e4780..24de49cbb 100644 --- a/integration-tests/typescript-nextjs/src/app/layout.tsx +++ b/integration-tests/typescript-nextjs/src/app/layout.tsx @@ -1,22 +1,22 @@ -import type { Metadata } from "next"; -import { Inter } from "next/font/google"; -import "./globals.css"; +import type {Metadata} from "next" +import {Inter} from "next/font/google" +import "./globals.css" -const inter = Inter({ subsets: ["latin"] }); +const inter = Inter({subsets: ["latin"]}) export const metadata: Metadata = { title: "Create Next App", description: "Generated by create next app", -}; +} export default function RootLayout({ children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode }>) { return ( {children} - ); + ) } diff --git a/integration-tests/typescript-nextjs/src/app/page.tsx b/integration-tests/typescript-nextjs/src/app/page.tsx index d2c63a496..c6b15f1be 100644 --- a/integration-tests/typescript-nextjs/src/app/page.tsx +++ b/integration-tests/typescript-nextjs/src/app/page.tsx @@ -1,5 +1,5 @@ -import Image from "next/image"; -import styles from "./page.module.css"; +import Image from "next/image" +import styles from "./page.module.css" export default function Home() { return ( @@ -91,5 +91,5 @@ export default function Home() { - ); + ) } diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts new file mode 100644 index 000000000..e5fef071d --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts @@ -0,0 +1,13 @@ +import { + _GET, + _POST, +} from "../../../generated/todo-lists.yaml/attachments/route" + +export const GET = _GET(async ({}, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) +export const POST = _POST(async ({ body }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts new file mode 100644 index 000000000..106807e70 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts @@ -0,0 +1,13 @@ +import { + _GET, + _POST, +} from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route" + +export const GET = _GET(async ({ params }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) +export const POST = _POST(async ({ params, body }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts new file mode 100644 index 000000000..17ad5c9d4 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts @@ -0,0 +1,18 @@ +import { + _GET, + _PUT, + _DELETE, +} from "../../../../generated/todo-lists.yaml/list/[listId]/route" + +export const GET = _GET(async ({ params }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) +export const PUT = _PUT(async ({ params, body }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) +export const DELETE = _DELETE(async ({ params }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts new file mode 100644 index 000000000..16410e64c --- /dev/null +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts @@ -0,0 +1,6 @@ +import { _GET } from "../../../generated/todo-lists.yaml/list/route" + +export const GET = _GET(async ({ query }, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({ message: "not implemented" } as any) +}) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts new file mode 100644 index 000000000..bdec23a52 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -0,0 +1,111 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { t_UnknownObject, t_UploadAttachmentBodySchema } from "../models" +import { + KoaRuntimeError, + RequestInputType, +} from "@nahkies/typescript-koa-runtime/errors" +import { + KoaRuntimeResponder, + KoaRuntimeResponse, + StatusCode, +} from "@nahkies/typescript-koa-runtime/server" +import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" +import { NextRequest } from "next/server" +import { z } from "zod" + +export type ListAttachmentsResponder = { + with200(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type ListAttachments = ( + params: Params, + respond: ListAttachmentsResponder, + ctx: { request: NextRequest }, +) => Promise> + +export type UploadAttachmentResponder = { + with202(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type UploadAttachment = ( + params: Params, + respond: UploadAttachmentResponder, + ctx: { request: NextRequest }, +) => Promise> + +export const _GET = + (implementation: ListAttachments) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: undefined, + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse(200) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } + +const uploadAttachmentBodySchema = z.object({ file: z.unknown().optional() }) + +export const _POST = + (implementation: UploadAttachment) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: undefined, + // TODO: this swallows repeated parameters + query: undefined, + body: parseRequestInput( + uploadAttachmentBodySchema, + await request.json(), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with202() { + return new KoaRuntimeResponse(202) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts new file mode 100644 index 000000000..e39c41ab9 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts @@ -0,0 +1,317 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_CreateUpdateTodoList, + t_Error, + t_Statuses, + t_TodoList, + t_UnknownObject, +} from "../models" +import { + AbstractFetchClient, + AbstractFetchClientConfig, + Res, + Server, + StatusCode, + StatusCode4xx, + StatusCode5xx, +} from "@nahkies/typescript-fetch-runtime/main" + +export class ApiClientServersOperations { + static listAttachments(url?: "{schema}://{tenant}.attachments.example.com"): { + build: ( + schema?: "http" | "https", + tenant?: string, + ) => Server<"listAttachments_ApiClient"> + } + static listAttachments(url?: "https://attachments.example.com"): { + build: () => Server<"listAttachments_ApiClient"> + } + static listAttachments( + url: string = "{schema}://{tenant}.attachments.example.com", + ): unknown { + switch (url) { + case "{schema}://{tenant}.attachments.example.com": + return { + build( + schema: "http" | "https" = "https", + tenant = "your-slug", + ): Server<"listAttachments_ApiClient"> { + return "{schema}://{tenant}.attachments.example.com" + .replace("{schema}", schema) + .replace( + "{tenant}", + tenant, + ) as Server<"listAttachments_ApiClient"> + }, + } + + case "https://attachments.example.com": + return { + build(): Server<"listAttachments_ApiClient"> { + return "https://attachments.example.com" as Server<"listAttachments_ApiClient"> + }, + } + + default: + throw new Error(`no matching server for url '${url}'`) + } + } + + static uploadAttachment( + url?: "{schema}://{tenant}.attachments.example.com", + ): { + build: ( + schema?: "http" | "https", + tenant?: string, + ) => Server<"uploadAttachment_ApiClient"> + } + static uploadAttachment(url?: "https://attachments.example.com"): { + build: () => Server<"uploadAttachment_ApiClient"> + } + static uploadAttachment( + url: string = "{schema}://{tenant}.attachments.example.com", + ): unknown { + switch (url) { + case "{schema}://{tenant}.attachments.example.com": + return { + build( + schema: "http" | "https" = "https", + tenant = "your-slug", + ): Server<"uploadAttachment_ApiClient"> { + return "{schema}://{tenant}.attachments.example.com" + .replace("{schema}", schema) + .replace( + "{tenant}", + tenant, + ) as Server<"uploadAttachment_ApiClient"> + }, + } + + case "https://attachments.example.com": + return { + build(): Server<"uploadAttachment_ApiClient"> { + return "https://attachments.example.com" as Server<"uploadAttachment_ApiClient"> + }, + } + + default: + throw new Error(`no matching server for url '${url}'`) + } + } +} + +export class ApiClientServers { + static default(): Server<"ApiClient"> { + return ApiClientServers.server().build() + } + + static server(url?: "{schema}://{tenant}.todo-lists.example.com"): { + build: (schema?: "http" | "https", tenant?: string) => Server<"ApiClient"> + } + static server(url?: "https://todo-lists.example.com"): { + build: () => Server<"ApiClient"> + } + static server( + url: string = "{schema}://{tenant}.todo-lists.example.com", + ): unknown { + switch (url) { + case "{schema}://{tenant}.todo-lists.example.com": + return { + build( + schema: "http" | "https" = "https", + tenant = "your-slug", + ): Server<"ApiClient"> { + return "{schema}://{tenant}.todo-lists.example.com" + .replace("{schema}", schema) + .replace("{tenant}", tenant) as Server<"ApiClient"> + }, + } + + case "https://todo-lists.example.com": + return { + build(): Server<"ApiClient"> { + return "https://todo-lists.example.com" as Server<"ApiClient"> + }, + } + + default: + throw new Error(`no matching server for url '${url}'`) + } + } + + static readonly operations = ApiClientServersOperations +} + +export interface ApiClientConfig extends AbstractFetchClientConfig { + basePath: Server<"ApiClient"> | string +} + +export class ApiClient extends AbstractFetchClient { + constructor(config: ApiClientConfig) { + super(config) + } + + async getTodoLists( + p: { + created?: string + statuses?: t_Statuses + tags?: string[] + } = {}, + timeout?: number, + opts: RequestInit = {}, + ): Promise> { + const url = this.basePath + `/list` + const headers = this._headers({}, opts.headers) + const query = this._query({ + created: p["created"], + statuses: p["statuses"], + tags: p["tags"], + }) + + return this._fetch( + url + query, + { method: "GET", ...opts, headers }, + timeout, + ) + } + + async getTodoListById( + p: { + listId: string + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + Res<200, t_TodoList> | Res | Res + > { + const url = this.basePath + `/list/${p["listId"]}` + const headers = this._headers({}, opts.headers) + + return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + } + + async updateTodoListById( + p: { + listId: string + requestBody: t_CreateUpdateTodoList + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + Res<200, t_TodoList> | Res | Res + > { + const url = this.basePath + `/list/${p["listId"]}` + const headers = this._headers( + { "Content-Type": "application/json" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._fetch(url, { method: "PUT", body, ...opts, headers }, timeout) + } + + async deleteTodoListById( + p: { + listId: string + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + Res<204, void> | Res | Res + > { + const url = this.basePath + `/list/${p["listId"]}` + const headers = this._headers({}, opts.headers) + + return this._fetch(url, { method: "DELETE", ...opts, headers }, timeout) + } + + async getTodoListItems( + p: { + listId: string + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise< + | Res< + 200, + { + completedAt?: string + content: string + createdAt: string + id: string + } + > + | Res< + StatusCode5xx, + { + code: string + message: string + } + > + > { + const url = this.basePath + `/list/${p["listId"]}/items` + const headers = this._headers({}, opts.headers) + + return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + } + + async createTodoListItem( + p: { + listId: string + requestBody: { + completedAt?: string + content: string + id: string + } + }, + timeout?: number, + opts: RequestInit = {}, + ): Promise> { + const url = this.basePath + `/list/${p["listId"]}/items` + const headers = this._headers( + { "Content-Type": "application/json" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._fetch(url, { method: "POST", body, ...opts, headers }, timeout) + } + + async listAttachments( + basePath: + | Server<"listAttachments_ApiClient"> + | string = ApiClientServers.operations.listAttachments().build(), + timeout?: number, + opts: RequestInit = {}, + ): Promise> { + const url = basePath + `/attachments` + const headers = this._headers({}, opts.headers) + + return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + } + + async uploadAttachment( + p: { + requestBody: { + file?: unknown + } + }, + basePath: + | Server<"uploadAttachment_ApiClient"> + | string = ApiClientServers.operations.uploadAttachment().build(), + timeout?: number, + opts: RequestInit = {}, + ): Promise> { + const url = basePath + `/attachments` + const headers = this._headers( + { "Content-Type": "multipart/form-data" }, + opts.headers, + ) + const body = JSON.stringify(p.requestBody) + + return this._fetch(url, { method: "POST", body, ...opts, headers }, timeout) + } +} diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts new file mode 100644 index 000000000..046cd0114 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts @@ -0,0 +1,157 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_CreateTodoListItemBodySchema, + t_CreateTodoListItemParamSchema, + t_GetTodoListItemsParamSchema, +} from "../../../models" +import { + KoaRuntimeError, + RequestInputType, +} from "@nahkies/typescript-koa-runtime/errors" +import { + KoaRuntimeResponder, + KoaRuntimeResponse, + StatusCode, + StatusCode5xx, +} from "@nahkies/typescript-koa-runtime/server" +import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" +import { NextRequest } from "next/server" +import { z } from "zod" + +export type GetTodoListItemsResponder = { + with200(): KoaRuntimeResponse<{ + completedAt?: string + content: string + createdAt: string + id: string + }> + withStatusCode5xx(status: StatusCode5xx): KoaRuntimeResponse<{ + code: string + message: string + }> +} & KoaRuntimeResponder + +export type GetTodoListItems = ( + params: Params, + respond: GetTodoListItemsResponder, + ctx: { request: NextRequest }, +) => Promise> + +export type CreateTodoListItemResponder = { + with204(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type CreateTodoListItem = ( + params: Params< + t_CreateTodoListItemParamSchema, + void, + t_CreateTodoListItemBodySchema, + void + >, + respond: CreateTodoListItemResponder, + ctx: { request: NextRequest }, +) => Promise> + +const getTodoListItemsParamSchema = z.object({ listId: z.string() }) + +export const _GET = + (implementation: GetTodoListItems) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: parseRequestInput( + getTodoListItemsParamSchema, + params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse<{ + completedAt?: string + content: string + createdAt: string + id: string + }>(200) + }, + withStatusCode5xx(status: StatusCode5xx) { + return new KoaRuntimeResponse<{ + code: string + message: string + }>(status) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } + +const createTodoListItemParamSchema = z.object({ listId: z.string() }) + +const createTodoListItemBodySchema = z.object({ + id: z.string(), + content: z.string(), + completedAt: z.string().datetime({ offset: true }).optional(), +}) + +export const _POST = + (implementation: CreateTodoListItem) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: parseRequestInput( + createTodoListItemParamSchema, + params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: parseRequestInput( + createTodoListItemBodySchema, + await request.json(), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with204() { + return new KoaRuntimeResponse(204) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts new file mode 100644 index 000000000..525de2b31 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts @@ -0,0 +1,211 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { + t_DeleteTodoListByIdParamSchema, + t_Error, + t_GetTodoListByIdParamSchema, + t_TodoList, + t_UpdateTodoListByIdBodySchema, + t_UpdateTodoListByIdParamSchema, +} from "../../models" +import { s_CreateUpdateTodoList } from "../../schemas" +import { + KoaRuntimeError, + RequestInputType, +} from "@nahkies/typescript-koa-runtime/errors" +import { + KoaRuntimeResponder, + KoaRuntimeResponse, + StatusCode, + StatusCode4xx, +} from "@nahkies/typescript-koa-runtime/server" +import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" +import { NextRequest } from "next/server" +import { z } from "zod" + +export type GetTodoListByIdResponder = { + with200(): KoaRuntimeResponse + withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse + withDefault(status: StatusCode): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type GetTodoListById = ( + params: Params, + respond: GetTodoListByIdResponder, + ctx: { request: NextRequest }, +) => Promise> + +export type UpdateTodoListByIdResponder = { + with200(): KoaRuntimeResponse + withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse + withDefault(status: StatusCode): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type UpdateTodoListById = ( + params: Params< + t_UpdateTodoListByIdParamSchema, + void, + t_UpdateTodoListByIdBodySchema, + void + >, + respond: UpdateTodoListByIdResponder, + ctx: { request: NextRequest }, +) => Promise> + +export type DeleteTodoListByIdResponder = { + with204(): KoaRuntimeResponse + withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse + withDefault(status: StatusCode): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type DeleteTodoListById = ( + params: Params, + respond: DeleteTodoListByIdResponder, + ctx: { request: NextRequest }, +) => Promise> + +const getTodoListByIdParamSchema = z.object({ listId: z.string() }) + +export const _GET = + (implementation: GetTodoListById) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: parseRequestInput( + getTodoListByIdParamSchema, + params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse(200) + }, + withStatusCode4xx(status: StatusCode4xx) { + return new KoaRuntimeResponse(status) + }, + withDefault(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } + +const updateTodoListByIdParamSchema = z.object({ listId: z.string() }) + +const updateTodoListByIdBodySchema = s_CreateUpdateTodoList + +export const _PUT = + (implementation: UpdateTodoListById) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: parseRequestInput( + updateTodoListByIdParamSchema, + params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: parseRequestInput( + updateTodoListByIdBodySchema, + await request.json(), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse(200) + }, + withStatusCode4xx(status: StatusCode4xx) { + return new KoaRuntimeResponse(status) + }, + withDefault(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } + +const deleteTodoListByIdParamSchema = z.object({ listId: z.string() }) + +export const _DELETE = + (implementation: DeleteTodoListById) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: parseRequestInput( + deleteTodoListByIdParamSchema, + params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with204() { + return new KoaRuntimeResponse(204) + }, + withStatusCode4xx(status: StatusCode4xx) { + return new KoaRuntimeResponse(status) + }, + withDefault(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts new file mode 100644 index 000000000..6d351ccd8 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -0,0 +1,82 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { t_GetTodoListsQuerySchema, t_TodoList } from "../models" +import { s_Statuses } from "../schemas" +import { + KoaRuntimeError, + RequestInputType, +} from "@nahkies/typescript-koa-runtime/errors" +import { + KoaRuntimeResponder, + KoaRuntimeResponse, + StatusCode, +} from "@nahkies/typescript-koa-runtime/server" +import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" +import { NextRequest } from "next/server" +import { z } from "zod" + +export type GetTodoListsResponder = { + with200(): KoaRuntimeResponse +} & KoaRuntimeResponder + +export type GetTodoLists = ( + params: Params, + respond: GetTodoListsResponder, + ctx: { request: NextRequest }, +) => Promise> + +const getTodoListsQuerySchema = z.object({ + created: z.string().datetime({ offset: true }).optional(), + statuses: z + .preprocess( + (it: unknown) => (Array.isArray(it) || it === undefined ? it : [it]), + s_Statuses, + ) + .optional(), + tags: z + .preprocess( + (it: unknown) => (Array.isArray(it) || it === undefined ? it : [it]), + z.array(z.string()), + ) + .optional(), +}) + +export const _GET = + (implementation: GetTodoLists) => + async ( + request: NextRequest, + { params }: { params: unknown }, + ): Promise => { + const input = { + params: undefined, + // TODO: this swallows repeated parameters + query: parseRequestInput( + getTodoListsQuerySchema, + Object.fromEntries(request.nextUrl.searchParams.entries()), + RequestInputType.QueryString, + ), + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new KoaRuntimeResponse(200) + }, + withStatus(status: StatusCode) { + return new KoaRuntimeResponse(status) + }, + } + + const { status, body } = await implementation(input, responder, { request }) + .then((it) => it.unpack()) + .catch((err) => { + throw KoaRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, { status }) + : new Response(undefined, { status }) + } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts new file mode 100644 index 000000000..1e323db01 --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/models.ts @@ -0,0 +1,67 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +export type t_Error = { + code?: number + message?: string +} + +export type t_CreateUpdateTodoList = { + name: string +} + +export type t_Statuses = ("incomplete" | "complete")[] + +export type t_TodoList = { + created: string + id: string + incompleteItemCount: number + name: string + totalItemCount: number + updated: string +} + +export type t_CreateTodoListItemBodySchema = { + completedAt?: string + content: string + id: string +} + +export type t_CreateTodoListItemParamSchema = { + listId: string +} + +export type t_DeleteTodoListByIdParamSchema = { + listId: string +} + +export type t_UnknownObject = { + [key: string]: unknown | undefined +} + +export type t_GetTodoListByIdParamSchema = { + listId: string +} + +export type t_GetTodoListItemsParamSchema = { + listId: string +} + +export type t_GetTodoListsQuerySchema = { + created?: string + statuses?: t_Statuses + tags?: string[] +} + +export type t_UpdateTodoListByIdBodySchema = { + name: string +} + +export type t_UpdateTodoListByIdParamSchema = { + listId: string +} + +export type t_UploadAttachmentBodySchema = { + file?: unknown +} diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts new file mode 100644 index 000000000..dd11359eb --- /dev/null +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts @@ -0,0 +1,25 @@ +/** AUTOGENERATED - DO NOT EDIT **/ +/* tslint:disable */ +/* eslint-disable */ + +import { z } from "zod" + +export const s_CreateUpdateTodoList = z.object({ name: z.string() }) + +export const s_Error = z.object({ + message: z.string().optional(), + code: z.coerce.number().optional(), +}) + +export const s_Statuses = z.array(z.enum(["incomplete", "complete"])) + +export const s_TodoList = z.object({ + id: z.string(), + name: z.string(), + totalItemCount: z.coerce.number(), + incompleteItemCount: z.coerce.number(), + created: z.string().datetime({ offset: true }), + updated: z.string().datetime({ offset: true }), +}) + +export const s_UnknownObject = z.record(z.unknown()) diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 5fc3bd8b0..8a7934e64 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -31,12 +31,12 @@ "@nahkies/openapi-code-generator": "*", "monaco-editor": "^0.52.2", "monaco-editor-auto-typings": "^0.4.6", - "next": "15.3.5", + "next": "^15.3.5", "nextra": "^4.2.17", "nextra-theme-docs": "^4.2.17", "node-polyfill-webpack-plugin": "^4.1.0", - "react": "19.1.0", - "react-dom": "19.1.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-hook-form": "^7.60.0" }, "devDependencies": { diff --git a/packages/documentation/src/lib/playground/load-runtime-types.tsx b/packages/documentation/src/lib/playground/load-runtime-types.tsx index 11c5ef05a..6d9057849 100644 --- a/packages/documentation/src/lib/playground/load-runtime-types.tsx +++ b/packages/documentation/src/lib/playground/load-runtime-types.tsx @@ -20,7 +20,8 @@ export const loadRuntimeTypes = async ( | "typescript-fetch" | "typescript-axios" | "typescript-koa" - | "typescript-express", + | "typescript-express" + | "typescript-nextjs", ) => { const fileRootPath = "file:///" @@ -115,6 +116,29 @@ export const loadRuntimeTypes = async ( path: "/node_modules/@nahkies/typescript-express-runtime/joi.d.ts", }, ], + // TODO: adjust + "typescript-nextjs": [ + { + uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/package.json", + path: "/node_modules/@nahkies/typescript-koa-runtime/package.json", + }, + { + uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/server.d.ts", + path: "/node_modules/@nahkies/typescript-koa-runtime/server.d.ts", + }, + { + uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/errors.d.ts", + path: "/node_modules/@nahkies/typescript-koa-runtime/errors.d.ts", + }, + { + uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/zod.d.ts", + path: "/node_modules/@nahkies/typescript-koa-runtime/zod.d.ts", + }, + { + uri: "https://unpkg.com/@nahkies/typescript-koa-runtime@latest/dist/joi.d.ts", + path: "/node_modules/@nahkies/typescript-koa-runtime/joi.d.ts", + }, + ], } for (const file of files[template]) { diff --git a/packages/openapi-code-generator/package.json b/packages/openapi-code-generator/package.json index e98d02ad4..cf9fc405c 100644 --- a/packages/openapi-code-generator/package.json +++ b/packages/openapi-code-generator/package.json @@ -53,6 +53,7 @@ "json5": "^2.2.3", "lodash": "^4.17.21", "source-map-support": "^0.5.21", + "ts-morph": "^22.0.0", "tslib": "^2.8.1", "typescript": "~5.8.3", "zod": "^3.25.74" diff --git a/packages/openapi-code-generator/src/config.ts b/packages/openapi-code-generator/src/config.ts index cc32f9f2d..792208a0c 100644 --- a/packages/openapi-code-generator/src/config.ts +++ b/packages/openapi-code-generator/src/config.ts @@ -17,6 +17,7 @@ export type Config = { | "typescript-angular" | "typescript-koa" | "typescript-express" + | "typescript-nextjs" schemaBuilder: "zod" | "joi" enableRuntimeResponseValidation: boolean enableTypedBasePaths: boolean diff --git a/packages/openapi-code-generator/src/core/input.ts b/packages/openapi-code-generator/src/core/input.ts index 5a4656a8b..194ab77b0 100644 --- a/packages/openapi-code-generator/src/core/input.ts +++ b/packages/openapi-code-generator/src/core/input.ts @@ -40,7 +40,11 @@ import { } from "./utils" export type OperationGroup = {name: string; operations: IROperation[]} -export type OperationGroupStrategy = "none" | "first-tag" | "first-slug" +export type OperationGroupStrategy = + | "none" + | "first-tag" + | "first-slug" + | "route" export type InputConfig = { extractInlineSchemas: boolean @@ -93,6 +97,8 @@ export class Input { return this.operationsByFirstTag() case "first-slug": return this.operationsByFirstSlug() + case "route": + return this.operationsByRoute() default: throw new Error(`unsupported grouping strategy '${strategy}'`) } @@ -197,6 +203,12 @@ export class Input { }) } + private operationsByRoute(): OperationGroup[] { + return this.groupOperations((operation) => { + return operation.route + }) + } + private groupOperations( groupBy: (operation: IROperation) => string | undefined, ): OperationGroup[] { diff --git a/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts b/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts index b87d2667d..8774284ce 100644 --- a/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts +++ b/packages/openapi-code-generator/src/core/loaders/tsconfig.loader.ts @@ -11,7 +11,7 @@ import { export type CompilerOptions = Pick< TsCompilerOptions, - "exactOptionalPropertyTypes" + "exactOptionalPropertyTypes" | "paths" > export async function loadTsConfigCompilerOptions( diff --git a/packages/openapi-code-generator/src/core/openapi-loader.ts b/packages/openapi-code-generator/src/core/openapi-loader.ts index 95a1fc012..103bab45c 100644 --- a/packages/openapi-code-generator/src/core/openapi-loader.ts +++ b/packages/openapi-code-generator/src/core/openapi-loader.ts @@ -26,7 +26,7 @@ export class OpenapiLoader { private readonly library = new Map() private constructor( - private readonly entryPointKey: string, + public readonly entryPointKey: string, private readonly config: {titleOverride: string | undefined}, private readonly validator: OpenapiValidator, private readonly genericLoader: GenericLoader, diff --git a/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts b/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts index f5faa343b..e57c394a5 100644 --- a/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts +++ b/packages/openapi-code-generator/src/core/schemas/tsconfig.schema.ts @@ -35,6 +35,7 @@ export const tsconfigSchema = z.object({ noPropertyAccessFromIndexSignature: z.boolean(), allowUnusedLabels: z.boolean(), allowUnreachableCode: z.boolean(), + paths: z.record(z.array(z.string())), }) .partial(), }) diff --git a/packages/openapi-code-generator/src/core/utils.ts b/packages/openapi-code-generator/src/core/utils.ts index 959ec1e2a..cf20c2722 100644 --- a/packages/openapi-code-generator/src/core/utils.ts +++ b/packages/openapi-code-generator/src/core/utils.ts @@ -5,6 +5,10 @@ export function isDefined(it: T | undefined): it is T { return it !== undefined } +export function isTruthy(it: T | undefined | null | "" | 0): it is T { + return Boolean(it) +} + export function hasSingleElement(it: T[]): it is [T] { return it.length === 1 } diff --git a/packages/openapi-code-generator/src/templates.ts b/packages/openapi-code-generator/src/templates.ts index 2c5d59355..9b9c27f67 100644 --- a/packages/openapi-code-generator/src/templates.ts +++ b/packages/openapi-code-generator/src/templates.ts @@ -4,6 +4,7 @@ import {generateTypescriptAxios} from "./typescript/client/typescript-axios/type import {generateTypescriptFetch} from "./typescript/client/typescript-fetch/typescript-fetch.generator" import {generateTypescriptExpress} from "./typescript/server/typescript-express/typescript-express.generator" import {generateTypescriptKoa} from "./typescript/server/typescript-koa/typescript-koa.generator" +import {generateTypescriptNextJS} from "./typescript/server/typescript-nextjs/typescript-nextjs.generator" export const templates = { "typescript-fetch": { @@ -31,6 +32,11 @@ export const templates = { type: "server", run: generateTypescriptExpress, }, + "typescript-nextjs": { + language: "typescript", + type: "server", + run: generateTypescriptNextJS, + }, } satisfies {[key: string]: OpenapiGenerator} export const templateNames = [ @@ -39,4 +45,5 @@ export const templateNames = [ "typescript-angular", "typescript-koa", "typescript-express", + "typescript-nextjs", ] as const satisfies Array diff --git a/packages/openapi-code-generator/src/typescript/common/compilation-units.ts b/packages/openapi-code-generator/src/typescript/common/compilation-units.ts index 43b4bc3b4..d6805114d 100644 --- a/packages/openapi-code-generator/src/typescript/common/compilation-units.ts +++ b/packages/openapi-code-generator/src/typescript/common/compilation-units.ts @@ -15,6 +15,7 @@ export class CompilationUnit { readonly filename: string, readonly imports: ImportBuilder | undefined, readonly code: string, + readonly isAutogenerated: boolean = true, ) {} hasCode() { @@ -26,7 +27,7 @@ export class CompilationUnit { includeHeader = true, }: {allowUnusedImports: boolean; includeHeader?: boolean}): string { return [ - includeHeader ? FILE_HEADER : "", + includeHeader && this.isAutogenerated ? FILE_HEADER : "", this.imports ? `${this.imports.toString(allowUnusedImports ? "" : this.code)}\n` : "", diff --git a/packages/openapi-code-generator/src/typescript/common/import-builder.ts b/packages/openapi-code-generator/src/typescript/common/import-builder.ts index a0aef4e6d..afc42c164 100644 --- a/packages/openapi-code-generator/src/typescript/common/import-builder.ts +++ b/packages/openapi-code-generator/src/typescript/common/import-builder.ts @@ -4,7 +4,10 @@ export class ImportBuilder { private readonly imports: Record> = {} private readonly importAll: Record = {} - constructor(private readonly unit?: {filename: string}) {} + constructor( + private readonly unit?: {filename: string}, + private readonly importAlias?: string, + ) {} from(from: string) { return { @@ -75,7 +78,7 @@ export class ImportBuilder { private add(name: string, from: string, isAll: boolean): void { // biome-ignore lint/style/noParameterAssign: - from = this.normalizeFrom(from) + from = this.normalizeFrom(from, this.unit?.filename) // biome-ignore lint/suspicious/noAssignInExpressions: const imports = (this.imports[from] = this.imports[from] ?? new Set()) @@ -87,14 +90,19 @@ export class ImportBuilder { } } - private normalizeFrom(from: string) { + public normalizeFrom(from: string, filename?: string) { if (from.endsWith(".ts")) { // biome-ignore lint/style/noParameterAssign: from = from.substring(0, from.length - ".ts".length) } - if (this.unit && from.startsWith("./")) { - const unitDirname = path.dirname(this.unit.filename) + // TODO: does this work on windows? + if (filename && from.startsWith("./")) { + if (this.importAlias) { + return this.importAlias + from.split(path.sep).slice(1).join(path.sep) + } + + const unitDirname = path.dirname(filename) const fromDirname = path.dirname(from) const relative = path.relative(unitDirname, fromDirname) diff --git a/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts b/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts index 7aeaaf2c4..24f88e21a 100644 --- a/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts +++ b/packages/openapi-code-generator/src/typescript/common/typescript-emitter.ts @@ -8,7 +8,7 @@ export class TypescriptEmitter { constructor( private readonly fsAdaptor: IFsAdaptor, private readonly formatter: IFormatter, - private readonly config: { + public readonly config: { destinationDirectory: string allowUnusedImports: boolean }, diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts new file mode 100644 index 000000000..b8d184136 --- /dev/null +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts @@ -0,0 +1,613 @@ +// biome-ignore lint/style/useNodejsImportProtocol: +import fs from "fs" +// biome-ignore lint/style/useNodejsImportProtocol: +import path from "path" +import _ from "lodash" +import { + Project, + type SourceFile, + StructureKind, + SyntaxKind, + VariableDeclarationKind, +} from "ts-morph" +import type {Input} from "../../../core/input" +import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" +import type { + IRModelObject, + IROperation, + IRParameter, +} from "../../../core/openapi-types-normalized" +import {isTruthy} from "../../../core/utils" +import { + type HttpMethod, + isDefined, + isHttpMethod, + titleCase, + upperFirst, +} from "../../../core/utils" +import type {OpenapiTypescriptGeneratorConfig} from "../../../templates.types" +import {TypescriptFetchClientBuilder} from "../../client/typescript-fetch/typescript-fetch-client-builder" +import {CompilationUnit, type ICompilable} from "../../common/compilation-units" +import {ImportBuilder} from "../../common/import-builder" +import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder" +import { + type SchemaBuilder, + schemaBuilderFactory, +} from "../../common/schema-builders/schema-builder" +import {ZodBuilder} from "../../common/schema-builders/zod-schema-builder" +import {TypeBuilder} from "../../common/type-builder" +import {intersect, object} from "../../common/type-utils" +import { + buildExport, + requestBodyAsParameter, + statusStringToType, +} from "../../common/typescript-common" + +function reduceParamsToOpenApiSchema(parameters: IRParameter[]): IRModelObject { + return parameters.reduce( + (acc, parameter) => { + acc.properties[parameter.name] = parameter.schema + + if (parameter.required) { + acc.required.push(parameter.name) + } + + return acc + }, + { + type: "object", + properties: {}, + required: [], + oneOf: [], + allOf: [], + anyOf: [], + additionalProperties: false, + nullable: false, + readOnly: false, + } as IRModelObject, + ) +} + +export class ServerRouterBuilder implements ICompilable { + private readonly statements: string[] = [] + private readonly operationTypes: { + operationId: string + statements: string[] + }[] = [] + + constructor( + public readonly filename: string, + private readonly name: string, + private readonly input: Input, + private readonly imports: ImportBuilder, + public readonly types: TypeBuilder, + public readonly schemaBuilder: SchemaBuilder, + ) { + // todo: unsure why, but adding an export at `.` of index.ts doesn't work properly + this.imports + .from("@nahkies/typescript-koa-runtime/server") + .add( + "startServer", + "ServerConfig", + "KoaRuntimeResponse", + "KoaRuntimeResponder", + "StatusCode2xx", + "StatusCode3xx", + "StatusCode4xx", + "StatusCode5xx", + "StatusCode", + ) + + this.imports.from("next/server").add("NextRequest") + + this.imports + .from("@nahkies/typescript-koa-runtime/errors") + .add("KoaRuntimeError", "RequestInputType") + + if (schemaBuilder instanceof ZodBuilder) { + imports + .from("@nahkies/typescript-koa-runtime/zod") + .add("parseRequestInput", "Params", "responseValidationFactory") + } else if (schemaBuilder instanceof JoiBuilder) { + imports + .from("@nahkies/typescript-koa-runtime/joi") + .add("parseRequestInput", "Params", "responseValidationFactory") + } + } + + add(operation: IROperation): void { + const types = this.types + const schemaBuilder = this.schemaBuilder + + const pathParams = operation.parameters.filter((it) => it.in === "path") + const paramSchema = pathParams.length + ? schemaBuilder.fromParameters(pathParams) + : undefined + let pathParamsType = "void" + + const queryParams = operation.parameters.filter((it) => it.in === "query") + const querySchema = queryParams.length + ? schemaBuilder.fromParameters(queryParams) + : undefined + let queryParamsType = "void" + + const headerParams = operation.parameters + .filter((it) => it.in === "header") + .map((it) => ({...it, name: it.name.toLowerCase()})) + const headerSchema = headerParams.length + ? schemaBuilder.fromParameters(headerParams) + : undefined + + let headerParamsType = "void" + + const {requestBodyParameter} = requestBodyAsParameter(operation) + const bodyParamIsRequired = Boolean(requestBodyParameter?.required) + const bodyParamSchema = requestBodyParameter + ? schemaBuilder.fromModel( + requestBodyParameter.schema, + requestBodyParameter.required, + true, + ) + : undefined + let bodyParamsType = "void" + + if (paramSchema) { + const name = `${operation.operationId}ParamSchema` + pathParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + _.upperFirst(name), + reduceParamsToOpenApiSchema(pathParams), + ), + ) + this.statements.push(`const ${name} = ${paramSchema.toString()}`) + } + + if (querySchema) { + const name = `${operation.operationId}QuerySchema` + queryParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + _.upperFirst(name), + reduceParamsToOpenApiSchema(queryParams), + ), + ) + this.statements.push(`const ${name} = ${querySchema.toString()}`) + } + + if (headerSchema) { + const name = `${operation.operationId}HeaderSchema` + + headerParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + upperFirst(name), + reduceParamsToOpenApiSchema(headerParams), + ), + ) + this.statements.push(`const ${name} = ${headerSchema.toString()}`) + } + + if (bodyParamSchema && requestBodyParameter) { + const name = `${operation.operationId}BodySchema` + bodyParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + _.upperFirst(name), + this.input.schema(requestBodyParameter.schema), + ), + ) + this.statements.push(`const ${name} = ${bodyParamSchema}`) + } + + const responseSchemas = Object.entries(operation.responses ?? {}).reduce( + (acc, [status, response]) => { + const content = Object.values(response.content ?? {}).pop() + + if (status === "default") { + acc.defaultResponse = { + schema: content + ? schemaBuilder.fromModel(content.schema, true, true) + : schemaBuilder.void(), + type: content ? types.schemaObjectToType(content.schema) : "void", + } + } else { + acc.specific.push({ + statusString: status, + statusType: statusStringToType(status), + type: content ? types.schemaObjectToType(content.schema) : "void", + schema: content + ? schemaBuilder.fromModel(content.schema, true, true) + : schemaBuilder.void(), + isWildCard: /^\d[xX]{2}$/.test(status), + }) + } + + return acc + }, + {specific: [], defaultResponse: undefined} as { + specific: { + statusString: string + statusType: string + schema: string + type: string + isWildCard: boolean + }[] + defaultResponse?: + | { + type: string + schema: string + } + | undefined + }, + ) + + this.operationTypes.push({ + operationId: operation.operationId, + statements: [ + buildExport({ + name: `${titleCase(operation.operationId)}Responder`, + value: intersect( + object([ + ...responseSchemas.specific.map((it) => + it.isWildCard + ? `with${it.statusType}(status: ${it.statusType}): KoaRuntimeResponse<${it.type}>` + : `with${it.statusType}(): KoaRuntimeResponse<${it.type}>`, + ), + responseSchemas.defaultResponse && + `withDefault(status: StatusCode): KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>`, + ]), + "KoaRuntimeResponder", + ), + kind: "type", + }), + buildExport({ + name: titleCase(operation.operationId), + value: `( + params: Params<${pathParamsType}, ${queryParamsType}, ${ + bodyParamsType + + (bodyParamsType === "void" || bodyParamIsRequired + ? "" + : " | undefined") + }, ${headerParamsType}>, + respond: ${titleCase(operation.operationId)}Responder, + ctx: {request: NextRequest} + ) => Promise>`, + kind: "type", + }), + ], + }) + + this.statements.push( + buildExport({ + name: `_${operation.method.toUpperCase()}`, + kind: "const", + value: `(implementation: ${titleCase(operation.operationId)}) => async (request: NextRequest, {params}: {params: unknown}): Promise => { + const input = { + params: ${ + paramSchema + ? `parseRequestInput(${operation.operationId}ParamSchema, params, RequestInputType.RouteParam)` + : "undefined" + }, + // TODO: this swallows repeated parameters + query: ${ + querySchema + ? `parseRequestInput(${operation.operationId}QuerySchema, Object.fromEntries(request.nextUrl.searchParams.entries()), RequestInputType.QueryString)` + : "undefined" + }, + body: ${ + bodyParamSchema + ? `parseRequestInput(${operation.operationId}BodySchema, await request.json(), RequestInputType.RequestBody)` + : "undefined" + }, + headers: ${ + headerSchema + ? `parseRequestInput(${operation.operationId}HeaderSchema, Reflect.get(ctx.request, "headers"), RequestInputType.RequestHeader)` + : "undefined" + } + } + + const responder = {${[ + ...responseSchemas.specific.map((it) => + it.isWildCard + ? `with${it.statusType}(status: ${it.statusType}) {return new KoaRuntimeResponse<${it.type}>(status) }` + : `with${it.statusType}() {return new KoaRuntimeResponse<${it.type}>(${it.statusType}) }`, + ), + responseSchemas.defaultResponse && + `withDefault(status: StatusCode) { return new KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>(status) }`, + "withStatus(status: StatusCode) { return new KoaRuntimeResponse(status)}", + ] + .filter(Boolean) + .join(",\n")}} + + const { + status, + body, + } = await implementation(input, responder, {request}) + .then(it => it.unpack()) + .catch(err => { throw KoaRuntimeError.HandlerError(err) }) + + return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) + }`, + }), + ) + } + + toString(): string { + const routes = this.statements + const code = ` +${this.operationTypes.flatMap((it) => it.statements).join("\n\n")} + +${routes.join("\n\n")} +` + return code + } + + toCompilationUnit(): CompilationUnit { + return new CompilationUnit(this.filename, this.imports, this.toString()) + } +} + +export class NextJSAppRouterBuilder implements ICompilable { + constructor( + public readonly filename: string, + private readonly imports: ImportBuilder, + private readonly companionFilename: string, + private readonly sourceFile: SourceFile, + ) {} + + private readonly httpMethodsUsed = new Set() + + add(operation: IROperation): void { + const sourceFile = this.sourceFile + + const hasPathParam = + operation.parameters.filter((it) => it.in === "path").length > 0 + const hasQueryParam = + operation.parameters.filter((it) => it.in === "query").length > 0 + const hasBodyParam = Boolean( + requestBodyAsParameter(operation).requestBodyParameter, + ) + + const wrappingMethod = `_${operation.method.toUpperCase()}` + + this.httpMethodsUsed.add(operation.method) + + // Get the existing function, or create a new default one + const variableDeclaration = + sourceFile + .getVariableDeclaration(operation.method.toUpperCase()) + ?.getVariableStatement() || + sourceFile.addVariableStatement({ + isExported: true, + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: operation.method.toUpperCase(), + kind: StructureKind.VariableDeclaration, + initializer: `${wrappingMethod}(async (input, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + })`, + }, + ], + }) + + // Replace the params based on what inputs we have + // biome-ignore lint/style/noNonNullAssertion: + const declarations = variableDeclaration.getDeclarations()[0]! + // biome-ignore lint/style/noNonNullAssertion: + const innerFunction = declarations + .getInitializerIfKindOrThrow(SyntaxKind.CallExpression) + .getArguments()[0]! + .asKind(SyntaxKind.ArrowFunction)! + + // biome-ignore lint/complexity/noForEach: + innerFunction?.getParameters().forEach((parameter) => { + parameter.remove() + }) + + innerFunction?.addParameter({ + name: `{${[ + hasPathParam ? "params" : undefined, + hasQueryParam ? "query" : undefined, + hasBodyParam ? "body" : undefined, + ] + .filter(isDefined) + .join(",")}}`, + }) + + innerFunction?.addParameter({name: "respond"}) + innerFunction?.addParameter({name: "context"}) + } + + toString(): string { + return this.sourceFile.getFullText() + } + + toCompilationUnit(): CompilationUnit { + // Reconcile imports - attempt to find an existing one and replace it with correct one + const imports = this.sourceFile.getImportDeclarations() + const from = this.imports.normalizeFrom( + `./${this.companionFilename}`, + `./${this.filename}`, + ) + // biome-ignore lint/complexity/noForEach: + imports + .filter((it) => it.getModuleSpecifierValue().includes(from)) + .forEach((it) => it.remove()) + + this.sourceFile.addImportDeclaration({ + namedImports: Array.from(this.httpMethodsUsed).map((it) => `_${it}`), + moduleSpecifier: from, + }) + + // Remove any methods that were removed from the spec + // biome-ignore lint/complexity/noForEach: + this.sourceFile + .getVariableDeclarations() + .filter((it) => { + const name = it.getName() + return isHttpMethod(name) && !this.httpMethodsUsed.has(name) + }) + .forEach((it) => it.remove()) + + return new CompilationUnit( + this.filename, + this.imports, + this.toString(), + false, + ) + } +} + +function findImportAlias(dest: string, compilerOptions: CompilerOptions) { + const relative = `./${path.relative(process.cwd(), dest)}/*` + + const alias = Object.entries(compilerOptions.paths || {}).find(([, paths]) => + paths.includes(relative), + ) + + return alias ? alias[0].replace("*", "") : undefined +} + +export async function generateTypescriptNextJS( + config: OpenapiTypescriptGeneratorConfig, +): Promise { + const {input, emitter, allowAny} = config + + const importAlias = findImportAlias( + config.emitter.config.destinationDirectory, + config.compilerOptions, + ) + + // biome-ignore lint/complexity/useLiteralKeys: + const subDirectory = process.env["OPENAPI_INTEGRATION_TESTS"] + ? path.basename(config.input.loader.entryPointKey) + : "" + + const appDirectory = [".", "app", subDirectory] + .filter(isTruthy) + .join(path.sep) + const generatedDirectory = [".", "generated", subDirectory] + .filter(isTruthy) + .join(path.sep) + + const rootTypeBuilder = await TypeBuilder.fromInput( + [generatedDirectory, "models.ts"].join(path.sep), + input, + config.compilerOptions, + {allowAny}, + ) + + const rootSchemaBuilder = await schemaBuilderFactory( + [generatedDirectory, "schemas.ts"].join(path.sep), + input, + config.schemaBuilder, + {allowAny}, + ) + + const project = new Project() + + const serverRouters = ( + await Promise.all( + input.groupedOperations("route").map(async (group) => { + const filename = path.join( + generatedDirectory, + routeToNextJSFilepath(group.name), + ) + + const imports = new ImportBuilder({filename}, importAlias) + + const routerBuilder = new ServerRouterBuilder( + filename, + group.name, + input, + imports, + rootTypeBuilder.withImports(imports), + rootSchemaBuilder.withImports(imports), + ) + + const nextJsAppRouterPath = path.join( + appDirectory, + routeToNextJSFilepath(group.name), + ) + + const existing = fs.existsSync( + path.join(emitter.config.destinationDirectory, nextJsAppRouterPath), + ) + ? fs + .readFileSync( + path.join( + emitter.config.destinationDirectory, + nextJsAppRouterPath, + ), + "utf-8", + ) + .toString() + : "" + const sourceFile = project.createSourceFile( + nextJsAppRouterPath, + existing, + ) + + const nextJSAppRouterBuilder = new NextJSAppRouterBuilder( + nextJsAppRouterPath, + imports, + filename, + sourceFile, + ) + + for (const operation of group.operations) { + routerBuilder.add(operation) + nextJSAppRouterBuilder.add(operation) + } + + return [ + routerBuilder.toCompilationUnit(), + nextJSAppRouterBuilder.toCompilationUnit(), + ] + }), + ) + ).flat() + + const clientOutputPath = [generatedDirectory, "clients", "client.ts"].join( + path.sep, + ) + const clientImportBuilder = new ImportBuilder( + {filename: clientOutputPath}, + importAlias, + ) + + const fetchClientBuilder = new TypescriptFetchClientBuilder( + clientOutputPath, + "ApiClient", + input, + clientImportBuilder, + rootTypeBuilder.withImports(clientImportBuilder), + rootSchemaBuilder.withImports(clientImportBuilder), + { + enableRuntimeResponseValidation: config.enableRuntimeResponseValidation, + enableTypedBasePaths: config.enableTypedBasePaths, + }, + ) + + input.allOperations().map((it) => fetchClientBuilder.add(it)) + + await emitter.emitGenerationResult([ + ...serverRouters, + fetchClientBuilder.toCompilationUnit(), + rootTypeBuilder.toCompilationUnit(), + rootSchemaBuilder.toCompilationUnit(), + ]) +} + +function routeToNextJSFilepath(route: string): string { + const parts = route + .split("/") + .map((part) => part.replaceAll("{", "[").replaceAll("}", "]")) + + parts.push("route.ts") + + return path.join(...parts) +} diff --git a/scripts/generate.mjs b/scripts/generate.mjs index 475ed4177..94ca91348 100644 --- a/scripts/generate.mjs +++ b/scripts/generate.mjs @@ -33,10 +33,20 @@ async function runSingle(templatePath, input) { const filename = path.basename(input) const template = path.basename(templatePath) + if (template === "typescript-nextjs" && !input.endsWith("todo-lists.yaml")) { + console.warn(`skipping generation of ${templatePath} using ${template}`) + return + } + + const output = + template === "typescript-nextjs" + ? `integration-tests/${template}/src` + : `integration-tests/${template}/src/generated/${filename}` + const args = [ `--input="${input}"`, `--input-type=${inputType}`, - `--output="integration-tests/${template}/src/generated/${filename}"`, + `--output="${output}"`, `--template="${template}"`, "--schema-builder=zod", ] @@ -63,15 +73,19 @@ function runCmd(cmd) { console.info(cmd) - const child = exec(cmd, (err, stdout, stderr) => { - if (err) { - err.output = output - reject(err) - return - } + const child = exec( + cmd, + {env: {...process.env, OPENAPI_INTEGRATION_TESTS: "true"}}, + (err, stdout, stderr) => { + if (err) { + err.output = output + reject(err) + return + } - resolve(output) - }) + resolve(output) + }, + ) child.stdout.on("data", (it) => { output.push(it.trim()) diff --git a/yarn.lock b/yarn.lock index 0996276b3..94c6460a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3853,13 +3853,13 @@ __metadata: gh-pages: "npm:^6.3.0" monaco-editor: "npm:^0.52.2" monaco-editor-auto-typings: "npm:^0.4.6" - next: "npm:15.3.5" + next: "npm:^15.3.5" nextra: "npm:^4.2.17" nextra-theme-docs: "npm:^4.2.17" node-polyfill-webpack-plugin: "npm:^4.1.0" null-loader: "npm:^4.0.1" - react: "npm:19.1.0" - react-dom: "npm:19.1.0" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" react-hook-form: "npm:^7.60.0" tsx: "npm:^4.20.3" typescript: "npm:^5.8.3" @@ -3896,6 +3896,7 @@ __metadata: json5: "npm:^2.2.3" lodash: "npm:^4.17.21" source-map-support: "npm:^0.5.21" + ts-morph: "npm:^22.0.0" tslib: "npm:^2.8.1" typescript: "npm:~5.8.3" zod: "npm:^3.25.74" @@ -4353,10 +4354,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:14.1.4": - version: 14.1.4 - resolution: "@next/env@npm:14.1.4" - checksum: 10/76db04d141aed6e4e7f64619f66b84b39a01fd698db735381b530347794b252d74f9d71fe6787402f986a5202e9a4ce1d9c2569fec7c56e67e346c0522883b8b +"@next/env@npm:15.3.1": + version: 15.3.1 + resolution: "@next/env@npm:15.3.1" + checksum: 10/2e617d1fd954d7e4b8200697ffc726107943114453b1d8197b232c1d328720d1e7f920945dddbb35f47459e1aa77b8cabecd4134aa2915a5a46dafa19bf7e239 languageName: node linkType: hard @@ -4367,9 +4368,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-darwin-arm64@npm:14.1.4" +"@next/swc-darwin-arm64@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-darwin-arm64@npm:15.3.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -4381,9 +4382,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-x64@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-darwin-x64@npm:14.1.4" +"@next/swc-darwin-x64@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-darwin-x64@npm:15.3.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -4395,9 +4396,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-linux-arm64-gnu@npm:14.1.4" +"@next/swc-linux-arm64-gnu@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-linux-arm64-gnu@npm:15.3.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -4409,9 +4410,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-linux-arm64-musl@npm:14.1.4" +"@next/swc-linux-arm64-musl@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-linux-arm64-musl@npm:15.3.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -4423,9 +4424,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-linux-x64-gnu@npm:14.1.4" +"@next/swc-linux-x64-gnu@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-linux-x64-gnu@npm:15.3.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -4437,9 +4438,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-linux-x64-musl@npm:14.1.4" +"@next/swc-linux-x64-musl@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-linux-x64-musl@npm:15.3.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -4451,9 +4452,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-win32-arm64-msvc@npm:14.1.4" +"@next/swc-win32-arm64-msvc@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-win32-arm64-msvc@npm:15.3.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -4465,16 +4466,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-win32-ia32-msvc@npm:14.1.4" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@next/swc-win32-x64-msvc@npm:14.1.4": - version: 14.1.4 - resolution: "@next/swc-win32-x64-msvc@npm:14.1.4" +"@next/swc-win32-x64-msvc@npm:15.3.1": + version: 15.3.1 + resolution: "@next/swc-win32-x64-msvc@npm:15.3.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -6009,15 +6003,6 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.2": - version: 0.5.2 - resolution: "@swc/helpers@npm:0.5.2" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10/3a3b179b3369acd26c5da89a0e779c756ae5231eb18a5507524c7abf955f488d34d86649f5b8417a0e19879688470d06319f5cfca2273d6d6b2046950e0d79af - languageName: node - linkType: hard - "@swc/helpers@npm:^0.5.0": version: 0.5.17 resolution: "@swc/helpers@npm:0.5.17" @@ -6090,6 +6075,18 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.23.0": + version: 0.23.0 + resolution: "@ts-morph/common@npm:0.23.0" + dependencies: + fast-glob: "npm:^3.3.2" + minimatch: "npm:^9.0.3" + mkdirp: "npm:^3.0.1" + path-browserify: "npm:^1.0.1" + checksum: 10/05eabbab5a63d71a7dac17202519d23d4d4ec30780364d4dc3096ca86291e19f0284d0592a6ee89ec257204075a985d00f4788d816a89c41d0c1e0c8d281c480 + languageName: node + linkType: hard + "@tsconfig/node24@npm:^24.0.1": version: 24.0.1 resolution: "@tsconfig/node24@npm:24.0.1" @@ -6866,21 +6863,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0": - version: 24.0.10 - resolution: "@types/node@npm:24.0.10" - dependencies: - undici-types: "npm:~7.8.0" - checksum: 10/ff8921c515d72fbc0a11ff282096e2d2e11ac04a2e9c7f765bcec5cb69cd367a88ab5dd556dc162ac98b9212957939b7ae80f12f3fc90db10c82135affd6d120 - languageName: node - linkType: hard - -"@types/node@npm:^20": - version: 20.12.5 - resolution: "@types/node@npm:20.12.5" +"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.15.3": + version: 22.15.3 + resolution: "@types/node@npm:22.15.3" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/7b647ea6679016e4e58e1aa439c46b610230ffcbe19173911fbf1d1fa329ec6fd1eeba4e3e2d8743206d3b00d5a0cad75f1c90189e1d1ec057eb48df1a1dd747 + undici-types: "npm:~6.21.0" + checksum: 10/6b4ff03c36598432b419980f828281aa16383e2de6eb61f73275495ef8d2cbf8cb5607659b4cae5ff8b2b2ff69913ea07ffcc0be029e4280b6e8bc138dc6629b languageName: node linkType: hard @@ -6909,13 +6897,6 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*": - version: 15.7.13 - resolution: "@types/prop-types@npm:15.7.13" - checksum: 10/8935cad87c683c665d09a055919d617fe951cb3b2d5c00544e3a913f861a2bd8d2145b51c9aa6d2457d19f3107ab40784c40205e757232f6a80cc8b1c815513c - languageName: node - linkType: hard - "@types/qs@npm:*, @types/qs@npm:^6.14.0": version: 6.14.0 resolution: "@types/qs@npm:6.14.0" @@ -6930,12 +6911,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18": - version: 18.2.24 - resolution: "@types/react-dom@npm:18.2.24" - dependencies: - "@types/react": "npm:*" - checksum: 10/bbd4005f2f65b7606505e9b8759b6e99e222d503602765594ea327893fb7061de8951279baef47a1932f04d94d1865daea05a32f9fcf6f9f1143dbabce5b33de +"@types/react-dom@npm:^19.1.3": + version: 19.1.3 + resolution: "@types/react-dom@npm:19.1.3" + peerDependencies: + "@types/react": ^19.0.0 + checksum: 10/e951e683ee5fd823e938a847372cdc161b8f15a88dd80b54b2d6fe9fa189862412e584609aca9355835c388a528b6e71283e7dc13071b22f75bdc4fa676a3b56 languageName: node linkType: hard @@ -6948,13 +6929,12 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18": - version: 18.2.74 - resolution: "@types/react@npm:18.2.74" +"@types/react@npm:^19.1.2": + version: 19.1.2 + resolution: "@types/react@npm:19.1.2" dependencies: - "@types/prop-types": "npm:*" csstype: "npm:^3.0.2" - checksum: 10/4057aa7d082d434f8e580e5aebd4007e5dbe7f8e9ae5e506a34a629e382070694a0401bf3f0d38fe8d64f4b38622e5794341e634b9739784deae19b037ae43fa + checksum: 10/17803797227d2fc07a2cd6c17d57b1ea9b01eb16eca6318be60852c8d7467b4b58e675742f53d77ff4a37621a5814f16847dede73999181cb7f9449c1784fab6 languageName: node linkType: hard @@ -9028,6 +9008,13 @@ __metadata: languageName: node linkType: hard +"code-block-writer@npm:^13.0.1": + version: 13.0.1 + resolution: "code-block-writer@npm:13.0.1" + checksum: 10/3da803b1149d05a09b99e150df0e6d2ac5007bcf2ddd23d72e8b3e827cb6b7cb69b695472cfbc8b46a2bca4e7c11636788b9a7e7d518f3b45d0bddcac240b4af + languageName: node + linkType: hard + "coffee-script@npm:^1.12.4": version: 1.12.7 resolution: "coffee-script@npm:1.12.7" @@ -14293,7 +14280,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": +"js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 10/af37d0d913fb56aec6dc0074c163cc71cd23c0b8aad5c2350747b6721d37ba118af35abdd8b33c47ec2800de07dedb16a527ca9c530ee004093e04958bd0cbf2 @@ -15204,17 +15191,6 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0": - version: 1.4.0 - resolution: "loose-envify@npm:1.4.0" - dependencies: - js-tokens: "npm:^3.0.0 || ^4.0.0" - bin: - loose-envify: cli.js - checksum: 10/6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 - languageName: node - linkType: hard - "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0, lru-cache@npm:^10.2.2": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -16439,6 +16415,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.3": + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 10/4cdc18d112b164084513e890d6323370db14c22249d536ad1854539577a895e690a27513dc346392f61a4a50afbbd8abc88f3f25558bfbbbb862cd56508b20f5 + languageName: node + linkType: hard + "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -16854,30 +16839,32 @@ __metadata: languageName: node linkType: hard -"next@npm:14.1.4": - version: 14.1.4 - resolution: "next@npm:14.1.4" +"next@npm:^15.3.1": + version: 15.3.1 + resolution: "next@npm:15.3.1" dependencies: - "@next/env": "npm:14.1.4" - "@next/swc-darwin-arm64": "npm:14.1.4" - "@next/swc-darwin-x64": "npm:14.1.4" - "@next/swc-linux-arm64-gnu": "npm:14.1.4" - "@next/swc-linux-arm64-musl": "npm:14.1.4" - "@next/swc-linux-x64-gnu": "npm:14.1.4" - "@next/swc-linux-x64-musl": "npm:14.1.4" - "@next/swc-win32-arm64-msvc": "npm:14.1.4" - "@next/swc-win32-ia32-msvc": "npm:14.1.4" - "@next/swc-win32-x64-msvc": "npm:14.1.4" - "@swc/helpers": "npm:0.5.2" + "@next/env": "npm:15.3.1" + "@next/swc-darwin-arm64": "npm:15.3.1" + "@next/swc-darwin-x64": "npm:15.3.1" + "@next/swc-linux-arm64-gnu": "npm:15.3.1" + "@next/swc-linux-arm64-musl": "npm:15.3.1" + "@next/swc-linux-x64-gnu": "npm:15.3.1" + "@next/swc-linux-x64-musl": "npm:15.3.1" + "@next/swc-win32-arm64-msvc": "npm:15.3.1" + "@next/swc-win32-x64-msvc": "npm:15.3.1" + "@swc/counter": "npm:0.1.3" + "@swc/helpers": "npm:0.5.15" busboy: "npm:1.6.0" caniuse-lite: "npm:^1.0.30001579" - graceful-fs: "npm:^4.2.11" postcss: "npm:8.4.31" - styled-jsx: "npm:5.1.1" + sharp: "npm:^0.34.1" + styled-jsx: "npm:5.1.6" peerDependencies: "@opentelemetry/api": ^1.1.0 - react: ^18.2.0 - react-dom: ^18.2.0 + "@playwright/test": ^1.41.2 + babel-plugin-react-compiler: "*" + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 sass: ^1.3.0 dependenciesMeta: "@next/swc-darwin-arm64": @@ -16894,22 +16881,26 @@ __metadata: optional: true "@next/swc-win32-arm64-msvc": optional: true - "@next/swc-win32-ia32-msvc": - optional: true "@next/swc-win32-x64-msvc": optional: true + sharp: + optional: true peerDependenciesMeta: "@opentelemetry/api": optional: true + "@playwright/test": + optional: true + babel-plugin-react-compiler: + optional: true sass: optional: true bin: next: dist/bin/next - checksum: 10/16dd0667d55caf0b9915c530e4ae950ae7fad42c22573f333cd23f2fee8243afa4d3e8093a1c7d07251ced150c0bed9cde273cac951b919594a8e2112d669266 + checksum: 10/bc7e432bc153cc8ff0ea12bf7c5d1163e049d53c9414053dd90143526a205bbee0315aec1f27d60c8e52ba157b5e086f0c065c3f387d3361a4125579d8a88723 languageName: node linkType: hard -"next@npm:15.3.5": +"next@npm:^15.3.5": version: 15.3.5 resolution: "next@npm:15.3.5" dependencies: @@ -18954,7 +18945,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:19.1.0": +"react-dom@npm:^19.1.0": version: 19.1.0 resolution: "react-dom@npm:19.1.0" dependencies: @@ -18965,18 +18956,6 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18": - version: 18.2.0 - resolution: "react-dom@npm:18.2.0" - dependencies: - loose-envify: "npm:^1.1.0" - scheduler: "npm:^0.23.0" - peerDependencies: - react: ^18.2.0 - checksum: 10/ca5e7762ec8c17a472a3605b6f111895c9f87ac7d43a610ab7024f68cd833d08eda0625ce02ec7178cc1f3c957cf0b9273cdc17aa2cd02da87544331c43b1d21 - languageName: node - linkType: hard - "react-hook-form@npm:^7.60.0": version: 7.60.0 resolution: "react-hook-form@npm:7.60.0" @@ -19003,22 +18982,13 @@ __metadata: languageName: node linkType: hard -"react@npm:19.1.0": +"react@npm:^19.1.0": version: 19.1.0 resolution: "react@npm:19.1.0" checksum: 10/d0180689826fd9de87e839c365f6f361c561daea397d61d724687cae88f432a307d1c0f53a0ee95ddbe3352c10dac41d7ff1ad85530fb24951b27a39e5398db4 languageName: node linkType: hard -"react@npm:^18": - version: 18.2.0 - resolution: "react@npm:18.2.0" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10/b9214a9bd79e99d08de55f8bef2b7fc8c39630be97c4e29d7be173d14a9a10670b5325e94485f74cd8bff4966ef3c78ee53c79a7b0b9b70cba20aa8973acc694 - languageName: node - linkType: hard - "read-cmd-shim@npm:4.0.0, read-cmd-shim@npm:^4.0.0": version: 4.0.0 resolution: "read-cmd-shim@npm:4.0.0" @@ -20016,15 +19986,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.0": - version: 0.23.0 - resolution: "scheduler@npm:0.23.0" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10/0c4557aa37bafca44ff21dc0ea7c92e2dbcb298bc62eae92b29a39b029134f02fb23917d6ebc8b1fa536b4184934314c20d8864d156a9f6357f3398aaf7bfda8 - languageName: node - linkType: hard - "scheduler@npm:^0.26.0": version: 0.26.0 resolution: "scheduler@npm:0.26.0" @@ -21053,22 +21014,6 @@ __metadata: languageName: node linkType: hard -"styled-jsx@npm:5.1.1": - version: 5.1.1 - resolution: "styled-jsx@npm:5.1.1" - dependencies: - client-only: "npm:0.0.1" - peerDependencies: - react: ">= 16.8.0 || 17.x.x || ^18.0.0-0" - peerDependenciesMeta: - "@babel/core": - optional: true - babel-plugin-macros: - optional: true - checksum: 10/4f6a5d0010770fdeea1183d919d528fd46c484e23c0535ef3e1dd49488116f639c594f3bd4440e3bc8a8686c9f8d53c5761599870ff039ede11a5c3bfe08a4be - languageName: node - linkType: hard - "styled-jsx@npm:5.1.6": version: 5.1.6 resolution: "styled-jsx@npm:5.1.6" @@ -21506,6 +21451,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^22.0.0": + version: 22.0.0 + resolution: "ts-morph@npm:22.0.0" + dependencies: + "@ts-morph/common": "npm:~0.23.0" + code-block-writer: "npm:^13.0.1" + checksum: 10/e5d81d0d8d990fa9f86e285bd4052bcfa462e2f798f7eda86e11afc7d884dfdb053998dcbf79942942e8032070f8b266745e017771674a169731494fe035e192 + languageName: node + linkType: hard + "tsconfig-paths@npm:^4.1.2": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" @@ -21781,13 +21736,13 @@ __metadata: version: 0.0.0-use.local resolution: "typescript-nextjs@workspace:integration-tests/typescript-nextjs" dependencies: - "@types/node": "npm:^20" - "@types/react": "npm:^18" - "@types/react-dom": "npm:^18" - next: "npm:14.1.4" - react: "npm:^18" - react-dom: "npm:^18" - typescript: "npm:^5" + "@types/node": "npm:^22.15.3" + "@types/react": "npm:^19.1.2" + "@types/react-dom": "npm:^19.1.3" + next: "npm:^15.3.1" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" + typescript: "npm:^5.8.3" languageName: unknown linkType: soft @@ -21801,16 +21756,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5": - version: 5.4.4 - resolution: "typescript@npm:5.4.4" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/bade322d88fd93c8179e262aca9ba7f7b4417c09117879819c87946578c782ab123e3acb4733046a6e38714c47ef927360045a1f9292a1bff3a05a6577d27ca2 - languageName: node - linkType: hard - "typescript@patch:typescript@npm%3A>=3 < 6#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin, typescript@patch:typescript@npm%3A~5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5" @@ -21821,16 +21766,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5#optional!builtin": - version: 5.4.4 - resolution: "typescript@patch:typescript@npm%3A5.4.4#optional!builtin::version=5.4.4&hash=5adc0c" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10/88aff3244c31d4c6ede05b4fd28732fc8935a7fc638f2a3dcbbb767d1ac98e4b077f21ec74bc97f43c9307bc3f27e2359def1d793f9918c3429a744408fd75b4 - languageName: node - linkType: hard - "ua-parser-js@npm:^0.7.30": version: 0.7.40 resolution: "ua-parser-js@npm:0.7.40" @@ -21856,13 +21791,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~5.26.4": - version: 5.26.5 - resolution: "undici-types@npm:5.26.5" - checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd - languageName: node - linkType: hard - "undici-types@npm:~6.21.0": version: 6.21.0 resolution: "undici-types@npm:6.21.0" @@ -21870,13 +21798,6 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.8.0": - version: 7.8.0 - resolution: "undici-types@npm:7.8.0" - checksum: 10/fcff3fbab234f067fbd69e374ee2c198ba74c364ceaf6d93db7ca267e784457b5518cd01d0d2329b075f412574205ea3172a9a675facb49b4c9efb7141cd80b7 - languageName: node - linkType: hard - "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.1 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" From 8ad8744edd9d892d8ef2512ff8cc8edd1dadc4c6 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Mon, 12 May 2025 08:45:31 +0100 Subject: [PATCH 04/15] refactor: split into multiple files --- .../typescript-nextjs-app-router-builder.ts | 124 +++++ .../typescript-nextjs-router-builder.ts | 297 +++++++++++ .../typescript-nextjs.generator.ts | 460 +----------------- 3 files changed, 427 insertions(+), 454 deletions(-) create mode 100644 packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts create mode 100644 packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts new file mode 100644 index 000000000..048c8ec00 --- /dev/null +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts @@ -0,0 +1,124 @@ +import { + type SourceFile, + StructureKind, + SyntaxKind, + VariableDeclarationKind, +} from "ts-morph" +import type {IROperation} from "../../../core/openapi-types-normalized" +import {type HttpMethod, isDefined, isHttpMethod} from "../../../core/utils" +import {CompilationUnit, type ICompilable} from "../../common/compilation-units" +import type {ImportBuilder} from "../../common/import-builder" +import {requestBodyAsParameter} from "../../common/typescript-common" + +export class TypescriptNextjsAppRouterBuilder implements ICompilable { + constructor( + public readonly filename: string, + private readonly imports: ImportBuilder, + private readonly companionFilename: string, + private readonly sourceFile: SourceFile, + ) {} + + private readonly httpMethodsUsed = new Set() + + add(operation: IROperation): void { + const sourceFile = this.sourceFile + + const hasPathParam = + operation.parameters.filter((it) => it.in === "path").length > 0 + const hasQueryParam = + operation.parameters.filter((it) => it.in === "query").length > 0 + const hasBodyParam = Boolean( + requestBodyAsParameter(operation).requestBodyParameter, + ) + + const wrappingMethod = `_${operation.method.toUpperCase()}` + + this.httpMethodsUsed.add(operation.method) + + // Get the existing function, or create a new default one + const variableDeclaration = + sourceFile + .getVariableDeclaration(operation.method.toUpperCase()) + ?.getVariableStatement() || + sourceFile.addVariableStatement({ + isExported: true, + declarationKind: VariableDeclarationKind.Const, + declarations: [ + { + name: operation.method.toUpperCase(), + kind: StructureKind.VariableDeclaration, + initializer: `${wrappingMethod}(async (input, respond, context) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + })`, + }, + ], + }) + + // Replace the params based on what inputs we have + // biome-ignore lint/style/noNonNullAssertion: + const declarations = variableDeclaration.getDeclarations()[0]! + // biome-ignore lint/style/noNonNullAssertion: + const innerFunction = declarations + .getInitializerIfKindOrThrow(SyntaxKind.CallExpression) + .getArguments()[0]! + .asKind(SyntaxKind.ArrowFunction)! + + // biome-ignore lint/complexity/noForEach: + innerFunction?.getParameters().forEach((parameter) => { + parameter.remove() + }) + + innerFunction?.addParameter({ + name: `{${[ + hasPathParam ? "params" : undefined, + hasQueryParam ? "query" : undefined, + hasBodyParam ? "body" : undefined, + ] + .filter(isDefined) + .join(",")}}`, + }) + + innerFunction?.addParameter({name: "respond"}) + innerFunction?.addParameter({name: "context"}) + } + + toString(): string { + return this.sourceFile.getFullText() + } + + toCompilationUnit(): CompilationUnit { + // Reconcile imports - attempt to find an existing one and replace it with correct one + const imports = this.sourceFile.getImportDeclarations() + const from = this.imports.normalizeFrom( + `./${this.companionFilename}`, + `./${this.filename}`, + ) + // biome-ignore lint/complexity/noForEach: + imports + .filter((it) => it.getModuleSpecifierValue().includes(from)) + .forEach((it) => it.remove()) + + this.sourceFile.addImportDeclaration({ + namedImports: Array.from(this.httpMethodsUsed).map((it) => `_${it}`), + moduleSpecifier: from, + }) + + // Remove any methods that were removed from the spec + // biome-ignore lint/complexity/noForEach: + this.sourceFile + .getVariableDeclarations() + .filter((it) => { + const name = it.getName() + return isHttpMethod(name) && !this.httpMethodsUsed.has(name) + }) + .forEach((it) => it.remove()) + + return new CompilationUnit( + this.filename, + this.imports, + this.toString(), + false, + ) + } +} diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts new file mode 100644 index 000000000..a4ae0d89d --- /dev/null +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts @@ -0,0 +1,297 @@ +import _ from "lodash" +import type {Input} from "../../../core/input" +import type {IROperation} from "../../../core/openapi-types-normalized" +import {titleCase, upperFirst} from "../../../core/utils" +import {CompilationUnit, type ICompilable} from "../../common/compilation-units" +import type {ImportBuilder} from "../../common/import-builder" +import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder" +import type {SchemaBuilder} from "../../common/schema-builders/schema-builder" +import {ZodBuilder} from "../../common/schema-builders/zod-schema-builder" +import type {TypeBuilder} from "../../common/type-builder" +import {intersect, object} from "../../common/type-utils" +import { + buildExport, + requestBodyAsParameter, + statusStringToType, +} from "../../common/typescript-common" +import {reduceParamsToOpenApiSchema} from "../server-operation-builder" + +export class TypescriptNextjsRouterBuilder implements ICompilable { + private readonly statements: string[] = [] + private readonly operationTypes: { + operationId: string + statements: string[] + }[] = [] + + constructor( + public readonly filename: string, + private readonly name: string, + private readonly input: Input, + private readonly imports: ImportBuilder, + public readonly types: TypeBuilder, + public readonly schemaBuilder: SchemaBuilder, + ) { + // todo: unsure why, but adding an export at `.` of index.ts doesn't work properly + this.imports + .from("@nahkies/typescript-koa-runtime/server") + .add( + "startServer", + "ServerConfig", + "KoaRuntimeResponse", + "KoaRuntimeResponder", + "StatusCode2xx", + "StatusCode3xx", + "StatusCode4xx", + "StatusCode5xx", + "StatusCode", + ) + + this.imports.from("next/server").add("NextRequest") + + this.imports + .from("@nahkies/typescript-koa-runtime/errors") + .add("KoaRuntimeError", "RequestInputType") + + if (schemaBuilder instanceof ZodBuilder) { + imports + .from("@nahkies/typescript-koa-runtime/zod") + .add("parseRequestInput", "Params", "responseValidationFactory") + } else if (schemaBuilder instanceof JoiBuilder) { + imports + .from("@nahkies/typescript-koa-runtime/joi") + .add("parseRequestInput", "Params", "responseValidationFactory") + } + } + + add(operation: IROperation): void { + const types = this.types + const schemaBuilder = this.schemaBuilder + + const pathParams = operation.parameters.filter((it) => it.in === "path") + const paramSchema = pathParams.length + ? schemaBuilder.fromParameters(pathParams) + : undefined + let pathParamsType = "void" + + const queryParams = operation.parameters.filter((it) => it.in === "query") + const querySchema = queryParams.length + ? schemaBuilder.fromParameters(queryParams) + : undefined + let queryParamsType = "void" + + const headerParams = operation.parameters + .filter((it) => it.in === "header") + .map((it) => ({...it, name: it.name.toLowerCase()})) + const headerSchema = headerParams.length + ? schemaBuilder.fromParameters(headerParams) + : undefined + + let headerParamsType = "void" + + const {requestBodyParameter} = requestBodyAsParameter(operation) + const bodyParamIsRequired = Boolean(requestBodyParameter?.required) + const bodyParamSchema = requestBodyParameter + ? schemaBuilder.fromModel( + requestBodyParameter.schema, + requestBodyParameter.required, + true, + ) + : undefined + let bodyParamsType = "void" + + if (paramSchema) { + const name = `${operation.operationId}ParamSchema` + pathParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + _.upperFirst(name), + reduceParamsToOpenApiSchema(pathParams), + ), + ) + this.statements.push(`const ${name} = ${paramSchema.toString()}`) + } + + if (querySchema) { + const name = `${operation.operationId}QuerySchema` + queryParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + _.upperFirst(name), + reduceParamsToOpenApiSchema(queryParams), + ), + ) + this.statements.push(`const ${name} = ${querySchema.toString()}`) + } + + if (headerSchema) { + const name = `${operation.operationId}HeaderSchema` + + headerParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + upperFirst(name), + reduceParamsToOpenApiSchema(headerParams), + ), + ) + this.statements.push(`const ${name} = ${headerSchema.toString()}`) + } + + if (bodyParamSchema && requestBodyParameter) { + const name = `${operation.operationId}BodySchema` + bodyParamsType = types.schemaObjectToType( + this.input.loader.addVirtualType( + operation.operationId, + _.upperFirst(name), + this.input.schema(requestBodyParameter.schema), + ), + ) + this.statements.push(`const ${name} = ${bodyParamSchema}`) + } + + const responseSchemas = Object.entries(operation.responses ?? {}).reduce( + (acc, [status, response]) => { + const content = Object.values(response.content ?? {}).pop() + + if (status === "default") { + acc.defaultResponse = { + schema: content + ? schemaBuilder.fromModel(content.schema, true, true) + : schemaBuilder.void(), + type: content ? types.schemaObjectToType(content.schema) : "void", + } + } else { + acc.specific.push({ + statusString: status, + statusType: statusStringToType(status), + type: content ? types.schemaObjectToType(content.schema) : "void", + schema: content + ? schemaBuilder.fromModel(content.schema, true, true) + : schemaBuilder.void(), + isWildCard: /^\d[xX]{2}$/.test(status), + }) + } + + return acc + }, + {specific: [], defaultResponse: undefined} as { + specific: { + statusString: string + statusType: string + schema: string + type: string + isWildCard: boolean + }[] + defaultResponse?: + | { + type: string + schema: string + } + | undefined + }, + ) + + this.operationTypes.push({ + operationId: operation.operationId, + statements: [ + buildExport({ + name: `${titleCase(operation.operationId)}Responder`, + value: intersect( + object([ + ...responseSchemas.specific.map((it) => + it.isWildCard + ? `with${it.statusType}(status: ${it.statusType}): KoaRuntimeResponse<${it.type}>` + : `with${it.statusType}(): KoaRuntimeResponse<${it.type}>`, + ), + responseSchemas.defaultResponse && + `withDefault(status: StatusCode): KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>`, + ]), + "KoaRuntimeResponder", + ), + kind: "type", + }), + buildExport({ + name: titleCase(operation.operationId), + value: `( + params: Params<${pathParamsType}, ${queryParamsType}, ${ + bodyParamsType + + (bodyParamsType === "void" || bodyParamIsRequired + ? "" + : " | undefined") + }, ${headerParamsType}>, + respond: ${titleCase(operation.operationId)}Responder, + ctx: {request: NextRequest} + ) => Promise>`, + kind: "type", + }), + ], + }) + + this.statements.push( + buildExport({ + name: `_${operation.method.toUpperCase()}`, + kind: "const", + value: `(implementation: ${titleCase(operation.operationId)}) => async (request: NextRequest, {params}: {params: unknown}): Promise => { + const input = { + params: ${ + paramSchema + ? `parseRequestInput(${operation.operationId}ParamSchema, params, RequestInputType.RouteParam)` + : "undefined" + }, + // TODO: this swallows repeated parameters + query: ${ + querySchema + ? `parseRequestInput(${operation.operationId}QuerySchema, Object.fromEntries(request.nextUrl.searchParams.entries()), RequestInputType.QueryString)` + : "undefined" + }, + body: ${ + bodyParamSchema + ? `parseRequestInput(${operation.operationId}BodySchema, await request.json(), RequestInputType.RequestBody)` + : "undefined" + }, + headers: ${ + headerSchema + ? `parseRequestInput(${operation.operationId}HeaderSchema, Reflect.get(ctx.request, "headers"), RequestInputType.RequestHeader)` + : "undefined" + } + } + + const responder = {${[ + ...responseSchemas.specific.map((it) => + it.isWildCard + ? `with${it.statusType}(status: ${it.statusType}) {return new KoaRuntimeResponse<${it.type}>(status) }` + : `with${it.statusType}() {return new KoaRuntimeResponse<${it.type}>(${it.statusType}) }`, + ), + responseSchemas.defaultResponse && + `withDefault(status: StatusCode) { return new KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>(status) }`, + "withStatus(status: StatusCode) { return new KoaRuntimeResponse(status)}", + ] + .filter(Boolean) + .join(",\n")}} + + const { + status, + body, + } = await implementation(input, responder, {request}) + .then(it => it.unpack()) + .catch(err => { throw KoaRuntimeError.HandlerError(err) }) + + return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) + }`, + }), + ) + } + + toString(): string { + const routes = this.statements + const code = ` +${this.operationTypes.flatMap((it) => it.statements).join("\n\n")} + +${routes.join("\n\n")} +` + return code + } + + toCompilationUnit(): CompilationUnit { + return new CompilationUnit(this.filename, this.imports, this.toString()) + } +} diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts index b8d184136..24bc00491 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts @@ -2,464 +2,16 @@ import fs from "fs" // biome-ignore lint/style/useNodejsImportProtocol: import path from "path" -import _ from "lodash" -import { - Project, - type SourceFile, - StructureKind, - SyntaxKind, - VariableDeclarationKind, -} from "ts-morph" -import type {Input} from "../../../core/input" +import {Project} from "ts-morph" import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" -import type { - IRModelObject, - IROperation, - IRParameter, -} from "../../../core/openapi-types-normalized" import {isTruthy} from "../../../core/utils" -import { - type HttpMethod, - isDefined, - isHttpMethod, - titleCase, - upperFirst, -} from "../../../core/utils" import type {OpenapiTypescriptGeneratorConfig} from "../../../templates.types" import {TypescriptFetchClientBuilder} from "../../client/typescript-fetch/typescript-fetch-client-builder" -import {CompilationUnit, type ICompilable} from "../../common/compilation-units" import {ImportBuilder} from "../../common/import-builder" -import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder" -import { - type SchemaBuilder, - schemaBuilderFactory, -} from "../../common/schema-builders/schema-builder" -import {ZodBuilder} from "../../common/schema-builders/zod-schema-builder" +import {schemaBuilderFactory} from "../../common/schema-builders/schema-builder" import {TypeBuilder} from "../../common/type-builder" -import {intersect, object} from "../../common/type-utils" -import { - buildExport, - requestBodyAsParameter, - statusStringToType, -} from "../../common/typescript-common" - -function reduceParamsToOpenApiSchema(parameters: IRParameter[]): IRModelObject { - return parameters.reduce( - (acc, parameter) => { - acc.properties[parameter.name] = parameter.schema - - if (parameter.required) { - acc.required.push(parameter.name) - } - - return acc - }, - { - type: "object", - properties: {}, - required: [], - oneOf: [], - allOf: [], - anyOf: [], - additionalProperties: false, - nullable: false, - readOnly: false, - } as IRModelObject, - ) -} - -export class ServerRouterBuilder implements ICompilable { - private readonly statements: string[] = [] - private readonly operationTypes: { - operationId: string - statements: string[] - }[] = [] - - constructor( - public readonly filename: string, - private readonly name: string, - private readonly input: Input, - private readonly imports: ImportBuilder, - public readonly types: TypeBuilder, - public readonly schemaBuilder: SchemaBuilder, - ) { - // todo: unsure why, but adding an export at `.` of index.ts doesn't work properly - this.imports - .from("@nahkies/typescript-koa-runtime/server") - .add( - "startServer", - "ServerConfig", - "KoaRuntimeResponse", - "KoaRuntimeResponder", - "StatusCode2xx", - "StatusCode3xx", - "StatusCode4xx", - "StatusCode5xx", - "StatusCode", - ) - - this.imports.from("next/server").add("NextRequest") - - this.imports - .from("@nahkies/typescript-koa-runtime/errors") - .add("KoaRuntimeError", "RequestInputType") - - if (schemaBuilder instanceof ZodBuilder) { - imports - .from("@nahkies/typescript-koa-runtime/zod") - .add("parseRequestInput", "Params", "responseValidationFactory") - } else if (schemaBuilder instanceof JoiBuilder) { - imports - .from("@nahkies/typescript-koa-runtime/joi") - .add("parseRequestInput", "Params", "responseValidationFactory") - } - } - - add(operation: IROperation): void { - const types = this.types - const schemaBuilder = this.schemaBuilder - - const pathParams = operation.parameters.filter((it) => it.in === "path") - const paramSchema = pathParams.length - ? schemaBuilder.fromParameters(pathParams) - : undefined - let pathParamsType = "void" - - const queryParams = operation.parameters.filter((it) => it.in === "query") - const querySchema = queryParams.length - ? schemaBuilder.fromParameters(queryParams) - : undefined - let queryParamsType = "void" - - const headerParams = operation.parameters - .filter((it) => it.in === "header") - .map((it) => ({...it, name: it.name.toLowerCase()})) - const headerSchema = headerParams.length - ? schemaBuilder.fromParameters(headerParams) - : undefined - - let headerParamsType = "void" - - const {requestBodyParameter} = requestBodyAsParameter(operation) - const bodyParamIsRequired = Boolean(requestBodyParameter?.required) - const bodyParamSchema = requestBodyParameter - ? schemaBuilder.fromModel( - requestBodyParameter.schema, - requestBodyParameter.required, - true, - ) - : undefined - let bodyParamsType = "void" - - if (paramSchema) { - const name = `${operation.operationId}ParamSchema` - pathParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - _.upperFirst(name), - reduceParamsToOpenApiSchema(pathParams), - ), - ) - this.statements.push(`const ${name} = ${paramSchema.toString()}`) - } - - if (querySchema) { - const name = `${operation.operationId}QuerySchema` - queryParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - _.upperFirst(name), - reduceParamsToOpenApiSchema(queryParams), - ), - ) - this.statements.push(`const ${name} = ${querySchema.toString()}`) - } - - if (headerSchema) { - const name = `${operation.operationId}HeaderSchema` - - headerParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - upperFirst(name), - reduceParamsToOpenApiSchema(headerParams), - ), - ) - this.statements.push(`const ${name} = ${headerSchema.toString()}`) - } - - if (bodyParamSchema && requestBodyParameter) { - const name = `${operation.operationId}BodySchema` - bodyParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - _.upperFirst(name), - this.input.schema(requestBodyParameter.schema), - ), - ) - this.statements.push(`const ${name} = ${bodyParamSchema}`) - } - - const responseSchemas = Object.entries(operation.responses ?? {}).reduce( - (acc, [status, response]) => { - const content = Object.values(response.content ?? {}).pop() - - if (status === "default") { - acc.defaultResponse = { - schema: content - ? schemaBuilder.fromModel(content.schema, true, true) - : schemaBuilder.void(), - type: content ? types.schemaObjectToType(content.schema) : "void", - } - } else { - acc.specific.push({ - statusString: status, - statusType: statusStringToType(status), - type: content ? types.schemaObjectToType(content.schema) : "void", - schema: content - ? schemaBuilder.fromModel(content.schema, true, true) - : schemaBuilder.void(), - isWildCard: /^\d[xX]{2}$/.test(status), - }) - } - - return acc - }, - {specific: [], defaultResponse: undefined} as { - specific: { - statusString: string - statusType: string - schema: string - type: string - isWildCard: boolean - }[] - defaultResponse?: - | { - type: string - schema: string - } - | undefined - }, - ) - - this.operationTypes.push({ - operationId: operation.operationId, - statements: [ - buildExport({ - name: `${titleCase(operation.operationId)}Responder`, - value: intersect( - object([ - ...responseSchemas.specific.map((it) => - it.isWildCard - ? `with${it.statusType}(status: ${it.statusType}): KoaRuntimeResponse<${it.type}>` - : `with${it.statusType}(): KoaRuntimeResponse<${it.type}>`, - ), - responseSchemas.defaultResponse && - `withDefault(status: StatusCode): KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>`, - ]), - "KoaRuntimeResponder", - ), - kind: "type", - }), - buildExport({ - name: titleCase(operation.operationId), - value: `( - params: Params<${pathParamsType}, ${queryParamsType}, ${ - bodyParamsType + - (bodyParamsType === "void" || bodyParamIsRequired - ? "" - : " | undefined") - }, ${headerParamsType}>, - respond: ${titleCase(operation.operationId)}Responder, - ctx: {request: NextRequest} - ) => Promise>`, - kind: "type", - }), - ], - }) - - this.statements.push( - buildExport({ - name: `_${operation.method.toUpperCase()}`, - kind: "const", - value: `(implementation: ${titleCase(operation.operationId)}) => async (request: NextRequest, {params}: {params: unknown}): Promise => { - const input = { - params: ${ - paramSchema - ? `parseRequestInput(${operation.operationId}ParamSchema, params, RequestInputType.RouteParam)` - : "undefined" - }, - // TODO: this swallows repeated parameters - query: ${ - querySchema - ? `parseRequestInput(${operation.operationId}QuerySchema, Object.fromEntries(request.nextUrl.searchParams.entries()), RequestInputType.QueryString)` - : "undefined" - }, - body: ${ - bodyParamSchema - ? `parseRequestInput(${operation.operationId}BodySchema, await request.json(), RequestInputType.RequestBody)` - : "undefined" - }, - headers: ${ - headerSchema - ? `parseRequestInput(${operation.operationId}HeaderSchema, Reflect.get(ctx.request, "headers"), RequestInputType.RequestHeader)` - : "undefined" - } - } - - const responder = {${[ - ...responseSchemas.specific.map((it) => - it.isWildCard - ? `with${it.statusType}(status: ${it.statusType}) {return new KoaRuntimeResponse<${it.type}>(status) }` - : `with${it.statusType}() {return new KoaRuntimeResponse<${it.type}>(${it.statusType}) }`, - ), - responseSchemas.defaultResponse && - `withDefault(status: StatusCode) { return new KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>(status) }`, - "withStatus(status: StatusCode) { return new KoaRuntimeResponse(status)}", - ] - .filter(Boolean) - .join(",\n")}} - - const { - status, - body, - } = await implementation(input, responder, {request}) - .then(it => it.unpack()) - .catch(err => { throw KoaRuntimeError.HandlerError(err) }) - - return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) - }`, - }), - ) - } - - toString(): string { - const routes = this.statements - const code = ` -${this.operationTypes.flatMap((it) => it.statements).join("\n\n")} - -${routes.join("\n\n")} -` - return code - } - - toCompilationUnit(): CompilationUnit { - return new CompilationUnit(this.filename, this.imports, this.toString()) - } -} - -export class NextJSAppRouterBuilder implements ICompilable { - constructor( - public readonly filename: string, - private readonly imports: ImportBuilder, - private readonly companionFilename: string, - private readonly sourceFile: SourceFile, - ) {} - - private readonly httpMethodsUsed = new Set() - - add(operation: IROperation): void { - const sourceFile = this.sourceFile - - const hasPathParam = - operation.parameters.filter((it) => it.in === "path").length > 0 - const hasQueryParam = - operation.parameters.filter((it) => it.in === "query").length > 0 - const hasBodyParam = Boolean( - requestBodyAsParameter(operation).requestBodyParameter, - ) - - const wrappingMethod = `_${operation.method.toUpperCase()}` - - this.httpMethodsUsed.add(operation.method) - - // Get the existing function, or create a new default one - const variableDeclaration = - sourceFile - .getVariableDeclaration(operation.method.toUpperCase()) - ?.getVariableStatement() || - sourceFile.addVariableStatement({ - isExported: true, - declarationKind: VariableDeclarationKind.Const, - declarations: [ - { - name: operation.method.toUpperCase(), - kind: StructureKind.VariableDeclaration, - initializer: `${wrappingMethod}(async (input, respond, context) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) - })`, - }, - ], - }) - - // Replace the params based on what inputs we have - // biome-ignore lint/style/noNonNullAssertion: - const declarations = variableDeclaration.getDeclarations()[0]! - // biome-ignore lint/style/noNonNullAssertion: - const innerFunction = declarations - .getInitializerIfKindOrThrow(SyntaxKind.CallExpression) - .getArguments()[0]! - .asKind(SyntaxKind.ArrowFunction)! - - // biome-ignore lint/complexity/noForEach: - innerFunction?.getParameters().forEach((parameter) => { - parameter.remove() - }) - - innerFunction?.addParameter({ - name: `{${[ - hasPathParam ? "params" : undefined, - hasQueryParam ? "query" : undefined, - hasBodyParam ? "body" : undefined, - ] - .filter(isDefined) - .join(",")}}`, - }) - - innerFunction?.addParameter({name: "respond"}) - innerFunction?.addParameter({name: "context"}) - } - - toString(): string { - return this.sourceFile.getFullText() - } - - toCompilationUnit(): CompilationUnit { - // Reconcile imports - attempt to find an existing one and replace it with correct one - const imports = this.sourceFile.getImportDeclarations() - const from = this.imports.normalizeFrom( - `./${this.companionFilename}`, - `./${this.filename}`, - ) - // biome-ignore lint/complexity/noForEach: - imports - .filter((it) => it.getModuleSpecifierValue().includes(from)) - .forEach((it) => it.remove()) - - this.sourceFile.addImportDeclaration({ - namedImports: Array.from(this.httpMethodsUsed).map((it) => `_${it}`), - moduleSpecifier: from, - }) - - // Remove any methods that were removed from the spec - // biome-ignore lint/complexity/noForEach: - this.sourceFile - .getVariableDeclarations() - .filter((it) => { - const name = it.getName() - return isHttpMethod(name) && !this.httpMethodsUsed.has(name) - }) - .forEach((it) => it.remove()) - - return new CompilationUnit( - this.filename, - this.imports, - this.toString(), - false, - ) - } -} +import {TypescriptNextjsAppRouterBuilder} from "./typescript-nextjs-app-router-builder" +import {TypescriptNextjsRouterBuilder} from "./typescript-nextjs-router-builder" function findImportAlias(dest: string, compilerOptions: CompilerOptions) { const relative = `./${path.relative(process.cwd(), dest)}/*` @@ -519,7 +71,7 @@ export async function generateTypescriptNextJS( const imports = new ImportBuilder({filename}, importAlias) - const routerBuilder = new ServerRouterBuilder( + const routerBuilder = new TypescriptNextjsRouterBuilder( filename, group.name, input, @@ -551,7 +103,7 @@ export async function generateTypescriptNextJS( existing, ) - const nextJSAppRouterBuilder = new NextJSAppRouterBuilder( + const nextJSAppRouterBuilder = new TypescriptNextjsAppRouterBuilder( nextJsAppRouterPath, imports, filename, From 169d89fcc6715ed0ccca74ca26c99dcdd7842f06 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 17 May 2025 09:11:19 +0100 Subject: [PATCH 05/15] refactor: use AbstractRouterBuilder --- .../todo-lists.yaml/attachments/route.ts | 13 +- .../list/[listId]/items/route.ts | 17 +- .../todo-lists.yaml/list/[listId]/route.ts | 25 +- .../generated/todo-lists.yaml/list/route.ts | 7 +- .../typescript-nextjs-router-builder.ts | 279 ++++++------------ 5 files changed, 119 insertions(+), 222 deletions(-) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts index bdec23a52..38a3e0eec 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -16,6 +16,7 @@ import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" import { NextRequest } from "next/server" import { z } from "zod" +// /attachments export type ListAttachmentsResponder = { with200(): KoaRuntimeResponse } & KoaRuntimeResponder @@ -23,7 +24,7 @@ export type ListAttachmentsResponder = { export type ListAttachments = ( params: Params, respond: ListAttachmentsResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> export type UploadAttachmentResponder = { @@ -33,14 +34,14 @@ export type UploadAttachmentResponder = { export type UploadAttachment = ( params: Params, respond: UploadAttachmentResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> export const _GET = (implementation: ListAttachments) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: undefined, @@ -59,7 +60,7 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) @@ -76,7 +77,7 @@ export const _POST = (implementation: UploadAttachment) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: undefined, @@ -99,7 +100,7 @@ export const _POST = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts index 046cd0114..7f4adf529 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts @@ -21,6 +21,7 @@ import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" import { NextRequest } from "next/server" import { z } from "zod" +// /list/{listId}/items export type GetTodoListItemsResponder = { with200(): KoaRuntimeResponse<{ completedAt?: string @@ -37,7 +38,7 @@ export type GetTodoListItemsResponder = { export type GetTodoListItems = ( params: Params, respond: GetTodoListItemsResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> export type CreateTodoListItemResponder = { @@ -52,7 +53,7 @@ export type CreateTodoListItem = ( void >, respond: CreateTodoListItemResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> const getTodoListItemsParamSchema = z.object({ listId: z.string() }) @@ -61,12 +62,12 @@ export const _GET = (implementation: GetTodoListItems) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: parseRequestInput( getTodoListItemsParamSchema, - params, + await params, RequestInputType.RouteParam, ), // TODO: this swallows repeated parameters @@ -95,7 +96,7 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) @@ -118,12 +119,12 @@ export const _POST = (implementation: CreateTodoListItem) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: parseRequestInput( createTodoListItemParamSchema, - params, + await params, RequestInputType.RouteParam, ), // TODO: this swallows repeated parameters @@ -145,7 +146,7 @@ export const _POST = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts index 525de2b31..0ce76a2af 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts @@ -25,6 +25,7 @@ import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" import { NextRequest } from "next/server" import { z } from "zod" +// /list/{listId} export type GetTodoListByIdResponder = { with200(): KoaRuntimeResponse withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse @@ -34,7 +35,7 @@ export type GetTodoListByIdResponder = { export type GetTodoListById = ( params: Params, respond: GetTodoListByIdResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> export type UpdateTodoListByIdResponder = { @@ -51,7 +52,7 @@ export type UpdateTodoListById = ( void >, respond: UpdateTodoListByIdResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> export type DeleteTodoListByIdResponder = { @@ -63,7 +64,7 @@ export type DeleteTodoListByIdResponder = { export type DeleteTodoListById = ( params: Params, respond: DeleteTodoListByIdResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> const getTodoListByIdParamSchema = z.object({ listId: z.string() }) @@ -72,12 +73,12 @@ export const _GET = (implementation: GetTodoListById) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: parseRequestInput( getTodoListByIdParamSchema, - params, + await params, RequestInputType.RouteParam, ), // TODO: this swallows repeated parameters @@ -101,7 +102,7 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) @@ -120,12 +121,12 @@ export const _PUT = (implementation: UpdateTodoListById) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: parseRequestInput( updateTodoListByIdParamSchema, - params, + await params, RequestInputType.RouteParam, ), // TODO: this swallows repeated parameters @@ -153,7 +154,7 @@ export const _PUT = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) @@ -170,12 +171,12 @@ export const _DELETE = (implementation: DeleteTodoListById) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: parseRequestInput( deleteTodoListByIdParamSchema, - params, + await params, RequestInputType.RouteParam, ), // TODO: this swallows repeated parameters @@ -199,7 +200,7 @@ export const _DELETE = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts index 6d351ccd8..a97f31e6f 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -17,6 +17,7 @@ import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" import { NextRequest } from "next/server" import { z } from "zod" +// /list export type GetTodoListsResponder = { with200(): KoaRuntimeResponse } & KoaRuntimeResponder @@ -24,7 +25,7 @@ export type GetTodoListsResponder = { export type GetTodoLists = ( params: Params, respond: GetTodoListsResponder, - ctx: { request: NextRequest }, + request: NextRequest, ) => Promise> const getTodoListsQuerySchema = z.object({ @@ -47,7 +48,7 @@ export const _GET = (implementation: GetTodoLists) => async ( request: NextRequest, - { params }: { params: unknown }, + { params }: { params: Promise }, ): Promise => { const input = { params: undefined, @@ -70,7 +71,7 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, { request }) + const { status, body } = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts index a4ae0d89d..bdd9ad95d 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts @@ -1,36 +1,37 @@ -import _ from "lodash" import type {Input} from "../../../core/input" -import type {IROperation} from "../../../core/openapi-types-normalized" -import {titleCase, upperFirst} from "../../../core/utils" -import {CompilationUnit, type ICompilable} from "../../common/compilation-units" +import {titleCase} from "../../../core/utils" import type {ImportBuilder} from "../../common/import-builder" import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder" import type {SchemaBuilder} from "../../common/schema-builders/schema-builder" import {ZodBuilder} from "../../common/schema-builders/zod-schema-builder" import type {TypeBuilder} from "../../common/type-builder" -import {intersect, object} from "../../common/type-utils" +import {constStatement} from "../../common/type-utils" +import {buildExport} from "../../common/typescript-common" import { - buildExport, - requestBodyAsParameter, - statusStringToType, -} from "../../common/typescript-common" -import {reduceParamsToOpenApiSchema} from "../server-operation-builder" + AbstractRouterBuilder, + type ServerSymbols, +} from "../abstract-router-builder" +import type {ServerOperationBuilder} from "../server-operation-builder" -export class TypescriptNextjsRouterBuilder implements ICompilable { - private readonly statements: string[] = [] +export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { private readonly operationTypes: { operationId: string statements: string[] }[] = [] + // biome-ignore lint/complexity/noUselessConstructor: constructor( - public readonly filename: string, - private readonly name: string, - private readonly input: Input, - private readonly imports: ImportBuilder, - public readonly types: TypeBuilder, - public readonly schemaBuilder: SchemaBuilder, + filename: string, + name: string, + input: Input, + imports: ImportBuilder, + types: TypeBuilder, + schemaBuilder: SchemaBuilder, ) { + super(filename, name, input, imports, types, schemaBuilder) + } + + protected buildImports(): void { // todo: unsure why, but adding an export at `.` of index.ts doesn't work properly this.imports .from("@nahkies/typescript-koa-runtime/server") @@ -46,232 +47,111 @@ export class TypescriptNextjsRouterBuilder implements ICompilable { "StatusCode", ) - this.imports.from("next/server").add("NextRequest") + this.imports.from("next/server").add("NextRequest", "NextResponse") this.imports .from("@nahkies/typescript-koa-runtime/errors") .add("KoaRuntimeError", "RequestInputType") - if (schemaBuilder instanceof ZodBuilder) { - imports + if (this.schemaBuilder instanceof ZodBuilder) { + this.imports .from("@nahkies/typescript-koa-runtime/zod") .add("parseRequestInput", "Params", "responseValidationFactory") - } else if (schemaBuilder instanceof JoiBuilder) { - imports + } else if (this.schemaBuilder instanceof JoiBuilder) { + this.imports .from("@nahkies/typescript-koa-runtime/joi") .add("parseRequestInput", "Params", "responseValidationFactory") } } - add(operation: IROperation): void { + protected buildOperation(builder: ServerOperationBuilder): string { + const statements: string[] = [] + const types = this.types const schemaBuilder = this.schemaBuilder - const pathParams = operation.parameters.filter((it) => it.in === "path") - const paramSchema = pathParams.length - ? schemaBuilder.fromParameters(pathParams) - : undefined - let pathParamsType = "void" - - const queryParams = operation.parameters.filter((it) => it.in === "query") - const querySchema = queryParams.length - ? schemaBuilder.fromParameters(queryParams) - : undefined - let queryParamsType = "void" - - const headerParams = operation.parameters - .filter((it) => it.in === "header") - .map((it) => ({...it, name: it.name.toLowerCase()})) - const headerSchema = headerParams.length - ? schemaBuilder.fromParameters(headerParams) - : undefined - - let headerParamsType = "void" + const symbols = this.operationSymbols(builder.operationId) + const params = builder.parameters(symbols) - const {requestBodyParameter} = requestBodyAsParameter(operation) - const bodyParamIsRequired = Boolean(requestBodyParameter?.required) - const bodyParamSchema = requestBodyParameter - ? schemaBuilder.fromModel( - requestBodyParameter.schema, - requestBodyParameter.required, - true, - ) - : undefined - let bodyParamsType = "void" - - if (paramSchema) { - const name = `${operation.operationId}ParamSchema` - pathParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - _.upperFirst(name), - reduceParamsToOpenApiSchema(pathParams), - ), - ) - this.statements.push(`const ${name} = ${paramSchema.toString()}`) + if (params.path.schema) { + statements.push(constStatement(symbols.paramSchema, params.path.schema)) } - - if (querySchema) { - const name = `${operation.operationId}QuerySchema` - queryParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - _.upperFirst(name), - reduceParamsToOpenApiSchema(queryParams), - ), - ) - this.statements.push(`const ${name} = ${querySchema.toString()}`) + if (params.query.schema) { + statements.push(constStatement(symbols.querySchema, params.query.schema)) } - - if (headerSchema) { - const name = `${operation.operationId}HeaderSchema` - - headerParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - upperFirst(name), - reduceParamsToOpenApiSchema(headerParams), - ), + if (params.header.schema) { + statements.push( + constStatement(symbols.requestHeaderSchema, params.header.schema), ) - this.statements.push(`const ${name} = ${headerSchema.toString()}`) } - - if (bodyParamSchema && requestBodyParameter) { - const name = `${operation.operationId}BodySchema` - bodyParamsType = types.schemaObjectToType( - this.input.loader.addVirtualType( - operation.operationId, - _.upperFirst(name), - this.input.schema(requestBodyParameter.schema), - ), + if (params.body.schema) { + statements.push( + constStatement(symbols.requestBodySchema, params.body.schema), ) - this.statements.push(`const ${name} = ${bodyParamSchema}`) } - const responseSchemas = Object.entries(operation.responses ?? {}).reduce( - (acc, [status, response]) => { - const content = Object.values(response.content ?? {}).pop() - - if (status === "default") { - acc.defaultResponse = { - schema: content - ? schemaBuilder.fromModel(content.schema, true, true) - : schemaBuilder.void(), - type: content ? types.schemaObjectToType(content.schema) : "void", - } - } else { - acc.specific.push({ - statusString: status, - statusType: statusStringToType(status), - type: content ? types.schemaObjectToType(content.schema) : "void", - schema: content - ? schemaBuilder.fromModel(content.schema, true, true) - : schemaBuilder.void(), - isWildCard: /^\d[xX]{2}$/.test(status), - }) - } - - return acc - }, - {specific: [], defaultResponse: undefined} as { - specific: { - statusString: string - statusType: string - schema: string - type: string - isWildCard: boolean - }[] - defaultResponse?: - | { - type: string - schema: string - } - | undefined - }, + const responseSchemas = builder.responseSchemas() + const responder = builder.responder( + // TODO: nextjs types + "KoaRuntimeResponder", + "KoaRuntimeResponse", ) this.operationTypes.push({ - operationId: operation.operationId, + operationId: builder.operationId, statements: [ buildExport({ - name: `${titleCase(operation.operationId)}Responder`, - value: intersect( - object([ - ...responseSchemas.specific.map((it) => - it.isWildCard - ? `with${it.statusType}(status: ${it.statusType}): KoaRuntimeResponse<${it.type}>` - : `with${it.statusType}(): KoaRuntimeResponse<${it.type}>`, - ), - responseSchemas.defaultResponse && - `withDefault(status: StatusCode): KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>`, - ]), - "KoaRuntimeResponder", - ), + name: symbols.responderName, + value: responder.type, kind: "type", }), buildExport({ - name: titleCase(operation.operationId), + name: symbols.implTypeName, value: `( - params: Params<${pathParamsType}, ${queryParamsType}, ${ - bodyParamsType + - (bodyParamsType === "void" || bodyParamIsRequired - ? "" - : " | undefined") - }, ${headerParamsType}>, - respond: ${titleCase(operation.operationId)}Responder, - ctx: {request: NextRequest} + params: ${params.type}, + respond: ${symbols.responderName}, + request: NextRequest, ) => Promise>`, kind: "type", }), ], }) - this.statements.push( + statements.push( buildExport({ - name: `_${operation.method.toUpperCase()}`, + name: `_${builder.method.toUpperCase()}`, kind: "const", - value: `(implementation: ${titleCase(operation.operationId)}) => async (request: NextRequest, {params}: {params: unknown}): Promise => { + value: `(implementation: ${symbols.implTypeName}) => async (request: NextRequest, {params}: {params: Promise}): Promise => { const input = { params: ${ - paramSchema - ? `parseRequestInput(${operation.operationId}ParamSchema, params, RequestInputType.RouteParam)` + params.path.schema + ? `parseRequestInput(${symbols.paramSchema}, await params, RequestInputType.RouteParam)` : "undefined" }, // TODO: this swallows repeated parameters query: ${ - querySchema - ? `parseRequestInput(${operation.operationId}QuerySchema, Object.fromEntries(request.nextUrl.searchParams.entries()), RequestInputType.QueryString)` + params.query.schema + ? `parseRequestInput(${symbols.querySchema}, Object.fromEntries(request.nextUrl.searchParams.entries()), RequestInputType.QueryString)` : "undefined" }, body: ${ - bodyParamSchema - ? `parseRequestInput(${operation.operationId}BodySchema, await request.json(), RequestInputType.RequestBody)` + params.body.schema + ? `parseRequestInput(${symbols.requestBodySchema}, await request.json(), RequestInputType.RequestBody)` : "undefined" }, headers: ${ - headerSchema - ? `parseRequestInput(${operation.operationId}HeaderSchema, Reflect.get(ctx.request, "headers"), RequestInputType.RequestHeader)` + params.header.schema + ? `parseRequestInput(${symbols.requestHeaderSchema}, Reflect.get(request, "headers"), RequestInputType.RequestHeader)` : "undefined" } } - const responder = {${[ - ...responseSchemas.specific.map((it) => - it.isWildCard - ? `with${it.statusType}(status: ${it.statusType}) {return new KoaRuntimeResponse<${it.type}>(status) }` - : `with${it.statusType}() {return new KoaRuntimeResponse<${it.type}>(${it.statusType}) }`, - ), - responseSchemas.defaultResponse && - `withDefault(status: StatusCode) { return new KoaRuntimeResponse<${responseSchemas.defaultResponse.type}>(status) }`, - "withStatus(status: StatusCode) { return new KoaRuntimeResponse(status)}", - ] - .filter(Boolean) - .join(",\n")}} + const responder = ${responder.implementation} const { status, body, - } = await implementation(input, responder, {request}) + } = await implementation(input, responder, request) .then(it => it.unpack()) .catch(err => { throw KoaRuntimeError.HandlerError(err) }) @@ -279,19 +159,32 @@ export class TypescriptNextjsRouterBuilder implements ICompilable { }`, }), ) + + return statements.join("\n\n") } - toString(): string { - const routes = this.statements - const code = ` + protected operationSymbols(operationId: string): ServerSymbols { + return { + implPropName: operationId, + implTypeName: titleCase(operationId), + responderName: `${titleCase(operationId)}Responder`, + paramSchema: `${operationId}ParamSchema`, + querySchema: `${operationId}QuerySchema`, + requestBodySchema: `${operationId}BodySchema`, + requestHeaderSchema: `${operationId}HeaderSchema`, + responseBodyValidator: `${operationId}ResponseValidator`, + } + } + + protected buildRouter( + routerName: string, + routerStatements: string[], + ): string { + return ` +// ${routerName} ${this.operationTypes.flatMap((it) => it.statements).join("\n\n")} -${routes.join("\n\n")} +${routerStatements.join("\n\n")} ` - return code - } - - toCompilationUnit(): CompilationUnit { - return new CompilationUnit(this.filename, this.imports, this.toString()) } } From 858fcf13376e1b46c324fecc082e06dc8866f821 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 17 May 2025 09:35:31 +0100 Subject: [PATCH 06/15] fix: smarter params, nextjs 15 support --- .../app/todo-lists.yaml/attachments/route.ts | 13 ++-- .../list/[listId]/items/route.ts | 8 +-- .../todo-lists.yaml/list/[listId]/route.ts | 14 ++-- .../src/app/todo-lists.yaml/list/route.ts | 6 +- .../todo-lists.yaml/attachments/route.ts | 13 +--- .../generated/todo-lists.yaml/list/route.ts | 5 +- .../server/server-operation-builder.ts | 12 +++- .../typescript-nextjs-app-router-builder.ts | 72 +++++++++++++------ .../typescript-nextjs-router-builder.ts | 21 +++--- .../typescript-nextjs.generator.ts | 4 ++ 10 files changed, 100 insertions(+), 68 deletions(-) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts index e5fef071d..17adde126 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts @@ -1,13 +1,10 @@ -import { - _GET, - _POST, -} from "../../../generated/todo-lists.yaml/attachments/route" +import {_GET, _POST} from "../../../generated/todo-lists.yaml/attachments/route" -export const GET = _GET(async ({}, respond, context) => { +export const GET = _GET(async (respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const POST = _POST(async ({ body }, respond, context) => { +export const POST = _POST(async ({body}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts index 106807e70..b3d877530 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts @@ -3,11 +3,11 @@ import { _POST, } from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route" -export const GET = _GET(async ({ params }, respond, context) => { +export const GET = _GET(async ({params}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const POST = _POST(async ({ params, body }, respond, context) => { +export const POST = _POST(async ({params, body}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts index 17ad5c9d4..d9fa40af0 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts @@ -1,18 +1,18 @@ import { + _DELETE, _GET, _PUT, - _DELETE, } from "../../../../generated/todo-lists.yaml/list/[listId]/route" -export const GET = _GET(async ({ params }, respond, context) => { +export const GET = _GET(async ({params}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const PUT = _PUT(async ({ params, body }, respond, context) => { +export const PUT = _PUT(async ({params, body}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const DELETE = _DELETE(async ({ params }, respond, context) => { +export const DELETE = _DELETE(async ({params}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts index 16410e64c..84539220c 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts @@ -1,6 +1,6 @@ -import { _GET } from "../../../generated/todo-lists.yaml/list/route" +import {_GET} from "../../../generated/todo-lists.yaml/list/route" -export const GET = _GET(async ({ query }, respond, context) => { +export const GET = _GET(async ({query}, respond, context) => { // TODO: implementation - return respond.withStatus(501).body({ message: "not implemented" } as any) + return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts index 38a3e0eec..61ecc5278 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -22,7 +22,6 @@ export type ListAttachmentsResponder = { } & KoaRuntimeResponder export type ListAttachments = ( - params: Params, respond: ListAttachmentsResponder, request: NextRequest, ) => Promise> @@ -39,10 +38,7 @@ export type UploadAttachment = ( export const _GET = (implementation: ListAttachments) => - async ( - request: NextRequest, - { params }: { params: Promise }, - ): Promise => { + async (request: NextRequest): Promise => { const input = { params: undefined, // TODO: this swallows repeated parameters @@ -60,7 +56,7 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, request) + const { status, body } = await implementation(responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) @@ -75,10 +71,7 @@ const uploadAttachmentBodySchema = z.object({ file: z.unknown().optional() }) export const _POST = (implementation: UploadAttachment) => - async ( - request: NextRequest, - { params }: { params: Promise }, - ): Promise => { + async (request: NextRequest): Promise => { const input = { params: undefined, // TODO: this swallows repeated parameters diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts index a97f31e6f..4cd58b721 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -46,10 +46,7 @@ const getTodoListsQuerySchema = z.object({ export const _GET = (implementation: GetTodoLists) => - async ( - request: NextRequest, - { params }: { params: Promise }, - ): Promise => { + async (request: NextRequest): Promise => { const input = { params: undefined, // TODO: this swallows repeated parameters diff --git a/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts b/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts index 545602c9a..62a7812ff 100644 --- a/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/server-operation-builder.ts @@ -59,6 +59,7 @@ export type ServerOperationResponseSchemas = { } export type Parameters = { + hasParams: boolean type: string path: { schema: string | undefined @@ -134,7 +135,16 @@ export class ServerOperationBuilder { ${header.type} >` - return {type, path, query, header, body} + return { + hasParams: Boolean( + path.schema || query.schema || header.schema || body.schema, + ), + type, + path, + query, + header, + body, + } } responseValidator(): string { diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts index 048c8ec00..6f87642c3 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts @@ -4,16 +4,29 @@ import { SyntaxKind, VariableDeclarationKind, } from "ts-morph" +import type {Input} from "../../../core/input" import type {IROperation} from "../../../core/openapi-types-normalized" -import {type HttpMethod, isDefined, isHttpMethod} from "../../../core/utils" +import { + type HttpMethod, + isDefined, + isHttpMethod, + titleCase, +} from "../../../core/utils" import {CompilationUnit, type ICompilable} from "../../common/compilation-units" import type {ImportBuilder} from "../../common/import-builder" -import {requestBodyAsParameter} from "../../common/typescript-common" +import type {SchemaBuilder} from "../../common/schema-builders/schema-builder" +import type {TypeBuilder} from "../../common/type-builder" +import type {ServerSymbols} from "../abstract-router-builder" +import {ServerOperationBuilder} from "../server-operation-builder" export class TypescriptNextjsAppRouterBuilder implements ICompilable { constructor( - public readonly filename: string, + private readonly filename: string, + private readonly name: string, + private readonly input: Input, private readonly imports: ImportBuilder, + private readonly types: TypeBuilder, + private readonly schemaBuilder: SchemaBuilder, private readonly companionFilename: string, private readonly sourceFile: SourceFile, ) {} @@ -21,16 +34,18 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { private readonly httpMethodsUsed = new Set() add(operation: IROperation): void { - const sourceFile = this.sourceFile - - const hasPathParam = - operation.parameters.filter((it) => it.in === "path").length > 0 - const hasQueryParam = - operation.parameters.filter((it) => it.in === "query").length > 0 - const hasBodyParam = Boolean( - requestBodyAsParameter(operation).requestBodyParameter, + const builder = new ServerOperationBuilder( + operation, + this.input, + this.types, + this.schemaBuilder, ) + const symbols = this.operationSymbols(builder.operationId) + const params = builder.parameters(symbols) + + const sourceFile = this.sourceFile + const wrappingMethod = `_${operation.method.toUpperCase()}` this.httpMethodsUsed.add(operation.method) @@ -69,20 +84,37 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { parameter.remove() }) - innerFunction?.addParameter({ - name: `{${[ - hasPathParam ? "params" : undefined, - hasQueryParam ? "query" : undefined, - hasBodyParam ? "body" : undefined, - ] - .filter(isDefined) - .join(",")}}`, - }) + if (params.hasParams) { + innerFunction?.addParameter({ + name: `{${[ + params.path.schema ? "params" : undefined, + params.query.schema ? "query" : undefined, + params.body.schema ? "body" : undefined, + params.header.schema ? "headers" : undefined, + ] + .filter(isDefined) + .join(",")}}`, + }) + } innerFunction?.addParameter({name: "respond"}) innerFunction?.addParameter({name: "context"}) } + // TODO: duplication - should be shared with router builder + protected operationSymbols(operationId: string): ServerSymbols { + return { + implPropName: operationId, + implTypeName: titleCase(operationId), + responderName: `${titleCase(operationId)}Responder`, + paramSchema: `${operationId}ParamSchema`, + querySchema: `${operationId}QuerySchema`, + requestBodySchema: `${operationId}BodySchema`, + requestHeaderSchema: `${operationId}HeaderSchema`, + responseBodyValidator: `${operationId}ResponseValidator`, + } + } + toString(): string { return this.sourceFile.getFullText() } diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts index bdd9ad95d..aaacdce87 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts @@ -1,5 +1,5 @@ import type {Input} from "../../../core/input" -import {titleCase} from "../../../core/utils" +import {isDefined, titleCase} from "../../../core/utils" import type {ImportBuilder} from "../../common/import-builder" import {JoiBuilder} from "../../common/schema-builders/joi-schema-builder" import type {SchemaBuilder} from "../../common/schema-builders/schema-builder" @@ -67,9 +67,6 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { protected buildOperation(builder: ServerOperationBuilder): string { const statements: string[] = [] - const types = this.types - const schemaBuilder = this.schemaBuilder - const symbols = this.operationSymbols(builder.operationId) const params = builder.parameters(symbols) @@ -107,11 +104,13 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { }), buildExport({ name: symbols.implTypeName, - value: `( - params: ${params.type}, - respond: ${symbols.responderName}, - request: NextRequest, - ) => Promise>`, + value: `(${[ + params.hasParams ? `params: ${params.type}` : undefined, + `respond: ${symbols.responderName}`, + "request: NextRequest", + ] + .filter(isDefined) + .join(",")}) => Promise>`, kind: "type", }), ], @@ -121,7 +120,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { buildExport({ name: `_${builder.method.toUpperCase()}`, kind: "const", - value: `(implementation: ${symbols.implTypeName}) => async (request: NextRequest, {params}: {params: Promise}): Promise => { + value: `(implementation: ${symbols.implTypeName}) => async (${["request: NextRequest", params.path.schema ? "{params}: {params: Promise}" : undefined].filter(isDefined).join(",")}): Promise => { const input = { params: ${ params.path.schema @@ -151,7 +150,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { const { status, body, - } = await implementation(input, responder, request) + } = await implementation(${[params.hasParams ? "input" : undefined, "responder", "request"].filter(isDefined).join(",")}) .then(it => it.unpack()) .catch(err => { throw KoaRuntimeError.HandlerError(err) }) diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts index 24bc00491..4e9a77a12 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts @@ -105,7 +105,11 @@ export async function generateTypescriptNextJS( const nextJSAppRouterBuilder = new TypescriptNextjsAppRouterBuilder( nextJsAppRouterPath, + group.name, + input, imports, + rootTypeBuilder.withImports(imports), + rootSchemaBuilder.withImports(imports), filename, sourceFile, ) From 44996ea18ecc2f8026d0c049bf4a558cfb554f5a Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sun, 1 Jun 2025 12:36:01 +0100 Subject: [PATCH 07/15] fix: align deps --- .../typescript-nextjs/package.json | 8 +- yarn.lock | 152 +----------------- 2 files changed, 9 insertions(+), 151 deletions(-) diff --git a/integration-tests/typescript-nextjs/package.json b/integration-tests/typescript-nextjs/package.json index 4afa64d1a..e54039bad 100644 --- a/integration-tests/typescript-nextjs/package.json +++ b/integration-tests/typescript-nextjs/package.json @@ -11,14 +11,14 @@ "validate": "tsc -p ./tsconfig.json" }, "dependencies": { - "next": "^15.3.1", + "next": "^15.3.5", "react": "^19.1.0", "react-dom": "^19.1.0" }, "devDependencies": { - "@types/node": "^22.15.3", - "@types/react": "^19.1.2", - "@types/react-dom": "^19.1.3", + "@types/node": "^22.16.0", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", "typescript": "^5.8.3" } } diff --git a/yarn.lock b/yarn.lock index 94c6460a6..506cc6c60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4354,13 +4354,6 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:15.3.1": - version: 15.3.1 - resolution: "@next/env@npm:15.3.1" - checksum: 10/2e617d1fd954d7e4b8200697ffc726107943114453b1d8197b232c1d328720d1e7f920945dddbb35f47459e1aa77b8cabecd4134aa2915a5a46dafa19bf7e239 - languageName: node - linkType: hard - "@next/env@npm:15.3.5": version: 15.3.5 resolution: "@next/env@npm:15.3.5" @@ -4368,13 +4361,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-darwin-arm64@npm:15.3.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@next/swc-darwin-arm64@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-darwin-arm64@npm:15.3.5" @@ -4382,13 +4368,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-x64@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-darwin-x64@npm:15.3.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@next/swc-darwin-x64@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-darwin-x64@npm:15.3.5" @@ -4396,13 +4375,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-linux-arm64-gnu@npm:15.3.1" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - "@next/swc-linux-arm64-gnu@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-arm64-gnu@npm:15.3.5" @@ -4410,13 +4382,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-linux-arm64-musl@npm:15.3.1" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - "@next/swc-linux-arm64-musl@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-arm64-musl@npm:15.3.5" @@ -4424,13 +4389,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-linux-x64-gnu@npm:15.3.1" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - "@next/swc-linux-x64-gnu@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-x64-gnu@npm:15.3.5" @@ -4438,13 +4396,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-linux-x64-musl@npm:15.3.1" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - "@next/swc-linux-x64-musl@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-linux-x64-musl@npm:15.3.5" @@ -4452,13 +4403,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-win32-arm64-msvc@npm:15.3.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@next/swc-win32-arm64-msvc@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-win32-arm64-msvc@npm:15.3.5" @@ -4466,13 +4410,6 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:15.3.1": - version: 15.3.1 - resolution: "@next/swc-win32-x64-msvc@npm:15.3.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@next/swc-win32-x64-msvc@npm:15.3.5": version: 15.3.5 resolution: "@next/swc-win32-x64-msvc@npm:15.3.5" @@ -6863,7 +6800,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^22.15.3": +"@types/node@npm:*, @types/node@npm:>=10.0.0": version: 22.15.3 resolution: "@types/node@npm:22.15.3" dependencies: @@ -6911,15 +6848,6 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^19.1.3": - version: 19.1.3 - resolution: "@types/react-dom@npm:19.1.3" - peerDependencies: - "@types/react": ^19.0.0 - checksum: 10/e951e683ee5fd823e938a847372cdc161b8f15a88dd80b54b2d6fe9fa189862412e584609aca9355835c388a528b6e71283e7dc13071b22f75bdc4fa676a3b56 - languageName: node - linkType: hard - "@types/react-dom@npm:^19.1.6": version: 19.1.6 resolution: "@types/react-dom@npm:19.1.6" @@ -6929,15 +6857,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^19.1.2": - version: 19.1.2 - resolution: "@types/react@npm:19.1.2" - dependencies: - csstype: "npm:^3.0.2" - checksum: 10/17803797227d2fc07a2cd6c17d57b1ea9b01eb16eca6318be60852c8d7467b4b58e675742f53d77ff4a37621a5814f16847dede73999181cb7f9449c1784fab6 - languageName: node - linkType: hard - "@types/react@npm:^19.1.8": version: 19.1.8 resolution: "@types/react@npm:19.1.8" @@ -16839,67 +16758,6 @@ __metadata: languageName: node linkType: hard -"next@npm:^15.3.1": - version: 15.3.1 - resolution: "next@npm:15.3.1" - dependencies: - "@next/env": "npm:15.3.1" - "@next/swc-darwin-arm64": "npm:15.3.1" - "@next/swc-darwin-x64": "npm:15.3.1" - "@next/swc-linux-arm64-gnu": "npm:15.3.1" - "@next/swc-linux-arm64-musl": "npm:15.3.1" - "@next/swc-linux-x64-gnu": "npm:15.3.1" - "@next/swc-linux-x64-musl": "npm:15.3.1" - "@next/swc-win32-arm64-msvc": "npm:15.3.1" - "@next/swc-win32-x64-msvc": "npm:15.3.1" - "@swc/counter": "npm:0.1.3" - "@swc/helpers": "npm:0.5.15" - busboy: "npm:1.6.0" - caniuse-lite: "npm:^1.0.30001579" - postcss: "npm:8.4.31" - sharp: "npm:^0.34.1" - styled-jsx: "npm:5.1.6" - peerDependencies: - "@opentelemetry/api": ^1.1.0 - "@playwright/test": ^1.41.2 - babel-plugin-react-compiler: "*" - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - dependenciesMeta: - "@next/swc-darwin-arm64": - optional: true - "@next/swc-darwin-x64": - optional: true - "@next/swc-linux-arm64-gnu": - optional: true - "@next/swc-linux-arm64-musl": - optional: true - "@next/swc-linux-x64-gnu": - optional: true - "@next/swc-linux-x64-musl": - optional: true - "@next/swc-win32-arm64-msvc": - optional: true - "@next/swc-win32-x64-msvc": - optional: true - sharp: - optional: true - peerDependenciesMeta: - "@opentelemetry/api": - optional: true - "@playwright/test": - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - bin: - next: dist/bin/next - checksum: 10/bc7e432bc153cc8ff0ea12bf7c5d1163e049d53c9414053dd90143526a205bbee0315aec1f27d60c8e52ba157b5e086f0c065c3f387d3361a4125579d8a88723 - languageName: node - linkType: hard - "next@npm:^15.3.5": version: 15.3.5 resolution: "next@npm:15.3.5" @@ -21736,10 +21594,10 @@ __metadata: version: 0.0.0-use.local resolution: "typescript-nextjs@workspace:integration-tests/typescript-nextjs" dependencies: - "@types/node": "npm:^22.15.3" - "@types/react": "npm:^19.1.2" - "@types/react-dom": "npm:^19.1.3" - next: "npm:^15.3.1" + "@types/node": "npm:^22.16.0" + "@types/react": "npm:^19.1.8" + "@types/react-dom": "npm:^19.1.6" + next: "npm:^15.3.5" react: "npm:^19.1.0" react-dom: "npm:^19.1.0" typescript: "npm:^5.8.3" From fd24e11a0deb8a5f1a60af5b6de62dfce4271462 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 13:03:23 +0100 Subject: [PATCH 08/15] chore: regenerate --- .../todo-lists.yaml/attachments/route.ts | 22 +++++------ .../todo-lists.yaml/clients/client.ts | 26 ++++++------- .../list/[listId]/items/route.ts | 28 +++++++------- .../todo-lists.yaml/list/[listId]/route.ts | 38 +++++++++---------- .../generated/todo-lists.yaml/list/route.ts | 18 ++++----- .../src/generated/todo-lists.yaml/schemas.ts | 8 ++-- 6 files changed, 68 insertions(+), 72 deletions(-) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts index 61ecc5278..184c1a860 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -2,7 +2,7 @@ /* tslint:disable */ /* eslint-disable */ -import { t_UnknownObject, t_UploadAttachmentBodySchema } from "../models" +import {t_UnknownObject, t_UploadAttachmentBodySchema} from "../models" import { KoaRuntimeError, RequestInputType, @@ -12,9 +12,9 @@ import { KoaRuntimeResponse, StatusCode, } from "@nahkies/typescript-koa-runtime/server" -import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" -import { NextRequest } from "next/server" -import { z } from "zod" +import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +import {NextRequest} from "next/server" +import {z} from "zod" // /attachments export type ListAttachmentsResponder = { @@ -56,18 +56,18 @@ export const _GET = }, } - const { status, body } = await implementation(responder, request) + const {status, body} = await implementation(responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } -const uploadAttachmentBodySchema = z.object({ file: z.unknown().optional() }) +const uploadAttachmentBodySchema = z.object({file: z.unknown().optional()}) export const _POST = (implementation: UploadAttachment) => @@ -93,13 +93,13 @@ export const _POST = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts index e39c41ab9..925e0e07e 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts @@ -171,11 +171,7 @@ export class ApiClient extends AbstractFetchClient { tags: p["tags"], }) - return this._fetch( - url + query, - { method: "GET", ...opts, headers }, - timeout, - ) + return this._fetch(url + query, {method: "GET", ...opts, headers}, timeout) } async getTodoListById( @@ -190,7 +186,7 @@ export class ApiClient extends AbstractFetchClient { const url = this.basePath + `/list/${p["listId"]}` const headers = this._headers({}, opts.headers) - return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + return this._fetch(url, {method: "GET", ...opts, headers}, timeout) } async updateTodoListById( @@ -205,12 +201,12 @@ export class ApiClient extends AbstractFetchClient { > { const url = this.basePath + `/list/${p["listId"]}` const headers = this._headers( - { "Content-Type": "application/json" }, + {"Content-Type": "application/json"}, opts.headers, ) const body = JSON.stringify(p.requestBody) - return this._fetch(url, { method: "PUT", body, ...opts, headers }, timeout) + return this._fetch(url, {method: "PUT", body, ...opts, headers}, timeout) } async deleteTodoListById( @@ -225,7 +221,7 @@ export class ApiClient extends AbstractFetchClient { const url = this.basePath + `/list/${p["listId"]}` const headers = this._headers({}, opts.headers) - return this._fetch(url, { method: "DELETE", ...opts, headers }, timeout) + return this._fetch(url, {method: "DELETE", ...opts, headers}, timeout) } async getTodoListItems( @@ -255,7 +251,7 @@ export class ApiClient extends AbstractFetchClient { const url = this.basePath + `/list/${p["listId"]}/items` const headers = this._headers({}, opts.headers) - return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + return this._fetch(url, {method: "GET", ...opts, headers}, timeout) } async createTodoListItem( @@ -272,12 +268,12 @@ export class ApiClient extends AbstractFetchClient { ): Promise> { const url = this.basePath + `/list/${p["listId"]}/items` const headers = this._headers( - { "Content-Type": "application/json" }, + {"Content-Type": "application/json"}, opts.headers, ) const body = JSON.stringify(p.requestBody) - return this._fetch(url, { method: "POST", body, ...opts, headers }, timeout) + return this._fetch(url, {method: "POST", body, ...opts, headers}, timeout) } async listAttachments( @@ -290,7 +286,7 @@ export class ApiClient extends AbstractFetchClient { const url = basePath + `/attachments` const headers = this._headers({}, opts.headers) - return this._fetch(url, { method: "GET", ...opts, headers }, timeout) + return this._fetch(url, {method: "GET", ...opts, headers}, timeout) } async uploadAttachment( @@ -307,11 +303,11 @@ export class ApiClient extends AbstractFetchClient { ): Promise> { const url = basePath + `/attachments` const headers = this._headers( - { "Content-Type": "multipart/form-data" }, + {"Content-Type": "multipart/form-data"}, opts.headers, ) const body = JSON.stringify(p.requestBody) - return this._fetch(url, { method: "POST", body, ...opts, headers }, timeout) + return this._fetch(url, {method: "POST", body, ...opts, headers}, timeout) } } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts index 7f4adf529..3c2393ce0 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts @@ -17,9 +17,9 @@ import { StatusCode, StatusCode5xx, } from "@nahkies/typescript-koa-runtime/server" -import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" -import { NextRequest } from "next/server" -import { z } from "zod" +import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +import {NextRequest} from "next/server" +import {z} from "zod" // /list/{listId}/items export type GetTodoListItemsResponder = { @@ -56,13 +56,13 @@ export type CreateTodoListItem = ( request: NextRequest, ) => Promise> -const getTodoListItemsParamSchema = z.object({ listId: z.string() }) +const getTodoListItemsParamSchema = z.object({listId: z.string()}) export const _GET = (implementation: GetTodoListItems) => async ( request: NextRequest, - { params }: { params: Promise }, + {params}: {params: Promise}, ): Promise => { const input = { params: parseRequestInput( @@ -96,30 +96,30 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } -const createTodoListItemParamSchema = z.object({ listId: z.string() }) +const createTodoListItemParamSchema = z.object({listId: z.string()}) const createTodoListItemBodySchema = z.object({ id: z.string(), content: z.string(), - completedAt: z.string().datetime({ offset: true }).optional(), + completedAt: z.string().datetime({offset: true}).optional(), }) export const _POST = (implementation: CreateTodoListItem) => async ( request: NextRequest, - { params }: { params: Promise }, + {params}: {params: Promise}, ): Promise => { const input = { params: parseRequestInput( @@ -146,13 +146,13 @@ export const _POST = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts index 0ce76a2af..a814e4e35 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts @@ -10,7 +10,7 @@ import { t_UpdateTodoListByIdBodySchema, t_UpdateTodoListByIdParamSchema, } from "../../models" -import { s_CreateUpdateTodoList } from "../../schemas" +import {s_CreateUpdateTodoList} from "../../schemas" import { KoaRuntimeError, RequestInputType, @@ -21,9 +21,9 @@ import { StatusCode, StatusCode4xx, } from "@nahkies/typescript-koa-runtime/server" -import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" -import { NextRequest } from "next/server" -import { z } from "zod" +import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +import {NextRequest} from "next/server" +import {z} from "zod" // /list/{listId} export type GetTodoListByIdResponder = { @@ -67,13 +67,13 @@ export type DeleteTodoListById = ( request: NextRequest, ) => Promise> -const getTodoListByIdParamSchema = z.object({ listId: z.string() }) +const getTodoListByIdParamSchema = z.object({listId: z.string()}) export const _GET = (implementation: GetTodoListById) => async ( request: NextRequest, - { params }: { params: Promise }, + {params}: {params: Promise}, ): Promise => { const input = { params: parseRequestInput( @@ -102,18 +102,18 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } -const updateTodoListByIdParamSchema = z.object({ listId: z.string() }) +const updateTodoListByIdParamSchema = z.object({listId: z.string()}) const updateTodoListByIdBodySchema = s_CreateUpdateTodoList @@ -121,7 +121,7 @@ export const _PUT = (implementation: UpdateTodoListById) => async ( request: NextRequest, - { params }: { params: Promise }, + {params}: {params: Promise}, ): Promise => { const input = { params: parseRequestInput( @@ -154,24 +154,24 @@ export const _PUT = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } -const deleteTodoListByIdParamSchema = z.object({ listId: z.string() }) +const deleteTodoListByIdParamSchema = z.object({listId: z.string()}) export const _DELETE = (implementation: DeleteTodoListById) => async ( request: NextRequest, - { params }: { params: Promise }, + {params}: {params: Promise}, ): Promise => { const input = { params: parseRequestInput( @@ -200,13 +200,13 @@ export const _DELETE = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts index 4cd58b721..fcc18aa45 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -2,8 +2,8 @@ /* tslint:disable */ /* eslint-disable */ -import { t_GetTodoListsQuerySchema, t_TodoList } from "../models" -import { s_Statuses } from "../schemas" +import {t_GetTodoListsQuerySchema, t_TodoList} from "../models" +import {s_Statuses} from "../schemas" import { KoaRuntimeError, RequestInputType, @@ -13,9 +13,9 @@ import { KoaRuntimeResponse, StatusCode, } from "@nahkies/typescript-koa-runtime/server" -import { Params, parseRequestInput } from "@nahkies/typescript-koa-runtime/zod" -import { NextRequest } from "next/server" -import { z } from "zod" +import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +import {NextRequest} from "next/server" +import {z} from "zod" // /list export type GetTodoListsResponder = { @@ -29,7 +29,7 @@ export type GetTodoLists = ( ) => Promise> const getTodoListsQuerySchema = z.object({ - created: z.string().datetime({ offset: true }).optional(), + created: z.string().datetime({offset: true}).optional(), statuses: z .preprocess( (it: unknown) => (Array.isArray(it) || it === undefined ? it : [it]), @@ -68,13 +68,13 @@ export const _GET = }, } - const { status, body } = await implementation(input, responder, request) + const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { throw KoaRuntimeError.HandlerError(err) }) return body !== undefined - ? Response.json(body, { status }) - : new Response(undefined, { status }) + ? Response.json(body, {status}) + : new Response(undefined, {status}) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts index dd11359eb..241573572 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/schemas.ts @@ -2,9 +2,9 @@ /* tslint:disable */ /* eslint-disable */ -import { z } from "zod" +import {z} from "zod" -export const s_CreateUpdateTodoList = z.object({ name: z.string() }) +export const s_CreateUpdateTodoList = z.object({name: z.string()}) export const s_Error = z.object({ message: z.string().optional(), @@ -18,8 +18,8 @@ export const s_TodoList = z.object({ name: z.string(), totalItemCount: z.coerce.number(), incompleteItemCount: z.coerce.number(), - created: z.string().datetime({ offset: true }), - updated: z.string().datetime({ offset: true }), + created: z.string().datetime({offset: true}), + updated: z.string().datetime({offset: true}), }) export const s_UnknownObject = z.record(z.unknown()) From aef5ad25ea132d05c3e81a5c60341d0a63bae123 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 13:19:15 +0100 Subject: [PATCH 09/15] fix: use fs adaptor --- packages/openapi-code-generator/src/index.ts | 1 + .../src/templates.types.ts | 2 + .../typescript-nextjs.generator.ts | 51 ++++++++++++------- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/packages/openapi-code-generator/src/index.ts b/packages/openapi-code-generator/src/index.ts index 7f530eb10..37b0da567 100644 --- a/packages/openapi-code-generator/src/index.ts +++ b/packages/openapi-code-generator/src/index.ts @@ -87,5 +87,6 @@ export async function generate( filenameConvention: config.filenameConvention, allowAny: config.tsAllowAny, serverImplementationMethod: config.tsServerImplementationMethod, + fsAdaptor, }) } diff --git a/packages/openapi-code-generator/src/templates.types.ts b/packages/openapi-code-generator/src/templates.types.ts index 567f31bc8..2aff32023 100644 --- a/packages/openapi-code-generator/src/templates.types.ts +++ b/packages/openapi-code-generator/src/templates.types.ts @@ -1,3 +1,4 @@ +import type {IFsAdaptor} from "./core/file-system/fs-adaptor" import type {Input, OperationGroupStrategy} from "./core/input" import type {CompilerOptions} from "./core/loaders/tsconfig.loader" import type {IdentifierConvention} from "./core/utils" @@ -19,6 +20,7 @@ export interface OpenapiGeneratorConfig { enableTypedBasePaths: boolean groupingStrategy: OperationGroupStrategy filenameConvention: IdentifierConvention + fsAdaptor: IFsAdaptor } export interface OpenapiTypescriptGeneratorConfig diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts index 4e9a77a12..966b0d6ce 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts @@ -1,8 +1,7 @@ // biome-ignore lint/style/useNodejsImportProtocol: -import fs from "fs" -// biome-ignore lint/style/useNodejsImportProtocol: import path from "path" -import {Project} from "ts-morph" +import {Project, type SourceFile} from "ts-morph" +import type {IFsAdaptor} from "../../../core/file-system/fs-adaptor" import type {CompilerOptions} from "../../../core/loaders/tsconfig.loader" import {isTruthy} from "../../../core/utils" import type {OpenapiTypescriptGeneratorConfig} from "../../../templates.types" @@ -41,6 +40,7 @@ export async function generateTypescriptNextJS( const appDirectory = [".", "app", subDirectory] .filter(isTruthy) .join(path.sep) + const generatedDirectory = [".", "generated", subDirectory] .filter(isTruthy) .join(path.sep) @@ -85,23 +85,12 @@ export async function generateTypescriptNextJS( routeToNextJSFilepath(group.name), ) - const existing = fs.existsSync( - path.join(emitter.config.destinationDirectory, nextJsAppRouterPath), - ) - ? fs - .readFileSync( - path.join( - emitter.config.destinationDirectory, - nextJsAppRouterPath, - ), - "utf-8", - ) - .toString() - : "" - const sourceFile = project.createSourceFile( + const sourceFile = await loadExistingRouteImplementation({ + fsAdaptor: config.fsAdaptor, + project, + destinationDirectory: emitter.config.destinationDirectory, nextJsAppRouterPath, - existing, - ) + }) const nextJSAppRouterBuilder = new TypescriptNextjsAppRouterBuilder( nextJsAppRouterPath, @@ -158,6 +147,30 @@ export async function generateTypescriptNextJS( ]) } +async function loadExistingRouteImplementation({ + fsAdaptor, + project, + destinationDirectory, + nextJsAppRouterPath, +}: { + fsAdaptor: IFsAdaptor + project: Project + destinationDirectory: string + nextJsAppRouterPath: string +}): Promise { + const exists = await fsAdaptor.exists( + path.join(destinationDirectory, nextJsAppRouterPath), + ) + + const source = exists + ? await fsAdaptor.readFile( + path.join(destinationDirectory, nextJsAppRouterPath), + ) + : "" + + return project.createSourceFile(nextJsAppRouterPath, source) +} + function routeToNextJSFilepath(route: string): string { const parts = route .split("/") From ad4cdc22a7c146bfe4992fe1df8e831d97f4da79 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 13:34:07 +0100 Subject: [PATCH 10/15] chore: upgrade ts-morph --- packages/openapi-code-generator/package.json | 2 +- .../typescript-nextjs.generator.ts | 2 +- yarn.lock | 604 +++--------------- 3 files changed, 82 insertions(+), 526 deletions(-) diff --git a/packages/openapi-code-generator/package.json b/packages/openapi-code-generator/package.json index cf9fc405c..7a134343e 100644 --- a/packages/openapi-code-generator/package.json +++ b/packages/openapi-code-generator/package.json @@ -53,7 +53,7 @@ "json5": "^2.2.3", "lodash": "^4.17.21", "source-map-support": "^0.5.21", - "ts-morph": "^22.0.0", + "ts-morph": "^26.0.0", "tslib": "^2.8.1", "typescript": "~5.8.3", "zod": "^3.25.74" diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts index 966b0d6ce..6f5975bf1 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts @@ -59,7 +59,7 @@ export async function generateTypescriptNextJS( {allowAny}, ) - const project = new Project() + const project = new Project({useInMemoryFileSystem: true}) const serverRouters = ( await Promise.all( diff --git a/yarn.lock b/yarn.lock index 506cc6c60..b7b393316 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1911,17 +1911,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.4.4": - version: 7.27.3 - resolution: "@babel/types@npm:7.27.3" - dependencies: - "@babel/helper-string-parser": "npm:^7.27.1" - "@babel/helper-validator-identifier": "npm:^7.27.1" - checksum: 10/a24e6accd85c4747b974b3d68a3210d0aa1180c1a77b287ffcb7401cd2edad7bdecadaeb40fe5191be3990c3a5252943f7de7c09da13ed269adbb054b97056ee - languageName: node - linkType: hard - -"@babel/types@npm:^7.27.6, @babel/types@npm:^7.27.7, @babel/types@npm:^7.28.0": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.24.7, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.27.7, @babel/types@npm:^7.28.0, @babel/types@npm:^7.4.4": version: 7.28.0 resolution: "@babel/types@npm:7.28.0" dependencies: @@ -2154,13 +2144,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/aix-ppc64@npm:0.25.3" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/aix-ppc64@npm:0.25.5" @@ -2168,13 +2151,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/android-arm64@npm:0.25.3" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm64@npm:0.25.5" @@ -2182,13 +2158,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/android-arm@npm:0.25.3" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm@npm:0.25.5" @@ -2196,13 +2165,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/android-x64@npm:0.25.3" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-x64@npm:0.25.5" @@ -2210,13 +2172,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/darwin-arm64@npm:0.25.3" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-arm64@npm:0.25.5" @@ -2224,13 +2179,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/darwin-x64@npm:0.25.3" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-x64@npm:0.25.5" @@ -2238,13 +2186,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/freebsd-arm64@npm:0.25.3" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-arm64@npm:0.25.5" @@ -2252,13 +2193,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/freebsd-x64@npm:0.25.3" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-x64@npm:0.25.5" @@ -2266,13 +2200,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-arm64@npm:0.25.3" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm64@npm:0.25.5" @@ -2280,13 +2207,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-arm@npm:0.25.3" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm@npm:0.25.5" @@ -2294,13 +2214,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-ia32@npm:0.25.3" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ia32@npm:0.25.5" @@ -2308,13 +2221,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-loong64@npm:0.25.3" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-loong64@npm:0.25.5" @@ -2322,13 +2228,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-mips64el@npm:0.25.3" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-mips64el@npm:0.25.5" @@ -2336,13 +2235,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-ppc64@npm:0.25.3" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ppc64@npm:0.25.5" @@ -2350,13 +2242,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-riscv64@npm:0.25.3" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-riscv64@npm:0.25.5" @@ -2364,13 +2249,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-s390x@npm:0.25.3" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-s390x@npm:0.25.5" @@ -2378,13 +2256,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/linux-x64@npm:0.25.3" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-x64@npm:0.25.5" @@ -2392,13 +2263,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/netbsd-arm64@npm:0.25.3" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-arm64@npm:0.25.5" @@ -2406,13 +2270,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/netbsd-x64@npm:0.25.3" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-x64@npm:0.25.5" @@ -2420,13 +2277,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/openbsd-arm64@npm:0.25.3" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-arm64@npm:0.25.5" @@ -2434,13 +2284,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/openbsd-x64@npm:0.25.3" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-x64@npm:0.25.5" @@ -2448,13 +2291,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/sunos-x64@npm:0.25.3" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/sunos-x64@npm:0.25.5" @@ -2462,13 +2298,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/win32-arm64@npm:0.25.3" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-arm64@npm:0.25.5" @@ -2476,13 +2305,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/win32-ia32@npm:0.25.3" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-ia32@npm:0.25.5" @@ -2490,13 +2312,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.3": - version: 0.25.3 - resolution: "@esbuild/win32-x64@npm:0.25.3" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-x64@npm:0.25.5" @@ -2549,20 +2364,13 @@ __metadata: languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.10": +"@floating-ui/utils@npm:^0.2.10, @floating-ui/utils@npm:^0.2.8": version: 0.2.10 resolution: "@floating-ui/utils@npm:0.2.10" checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae languageName: node linkType: hard -"@floating-ui/utils@npm:^0.2.8": - version: 0.2.9 - resolution: "@floating-ui/utils@npm:0.2.9" - checksum: 10/0ca786347db3dd8d9034b86d1449fabb96642788e5900cc5f2aee433cd7b243efbcd7a165bead50b004ee3f20a90ddebb6a35296fc41d43cfd361b6f01b69ffb - languageName: node - linkType: hard - "@formatjs/intl-localematcher@npm:^0.6.0": version: 0.6.1 resolution: "@formatjs/intl-localematcher@npm:0.6.1" @@ -3113,6 +2921,22 @@ __metadata: languageName: node linkType: hard +"@isaacs/balanced-match@npm:^4.0.1": + version: 4.0.1 + resolution: "@isaacs/balanced-match@npm:4.0.1" + checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 + languageName: node + linkType: hard + +"@isaacs/brace-expansion@npm:^5.0.0": + version: 5.0.0 + resolution: "@isaacs/brace-expansion@npm:5.0.0" + dependencies: + "@isaacs/balanced-match": "npm:^4.0.1" + checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -3461,7 +3285,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.12": +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": version: 0.3.12 resolution: "@jridgewell/gen-mapping@npm:0.3.12" dependencies: @@ -3471,17 +3295,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.8 - resolution: "@jridgewell/gen-mapping@npm:0.3.8" - dependencies: - "@jridgewell/set-array": "npm:^1.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" - "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10/9d3a56ab3612ab9b85d38b2a93b87f3324f11c5130859957f6500e4ac8ce35f299d5ccc3ecd1ae87597601ecf83cee29e9afd04c18777c24011073992ff946df - languageName: node - linkType: hard - "@jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" @@ -3489,13 +3302,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.2.1": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.10 resolution: "@jridgewell/source-map@npm:0.3.10" @@ -3506,13 +3312,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10": - version: 1.5.0 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" - checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd - languageName: node - linkType: hard - "@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.4 resolution: "@jridgewell/sourcemap-codec@npm:1.5.4" @@ -3896,7 +3695,7 @@ __metadata: json5: "npm:^2.2.3" lodash: "npm:^4.17.21" source-map-support: "npm:^0.5.21" - ts-morph: "npm:^22.0.0" + ts-morph: "npm:^26.0.0" tslib: "npm:^2.8.1" typescript: "npm:~5.8.3" zod: "npm:^3.25.74" @@ -6012,15 +5811,14 @@ __metadata: languageName: node linkType: hard -"@ts-morph/common@npm:~0.23.0": - version: 0.23.0 - resolution: "@ts-morph/common@npm:0.23.0" +"@ts-morph/common@npm:~0.27.0": + version: 0.27.0 + resolution: "@ts-morph/common@npm:0.27.0" dependencies: - fast-glob: "npm:^3.3.2" - minimatch: "npm:^9.0.3" - mkdirp: "npm:^3.0.1" + fast-glob: "npm:^3.3.3" + minimatch: "npm:^10.0.1" path-browserify: "npm:^1.0.1" - checksum: 10/05eabbab5a63d71a7dac17202519d23d4d4ec30780364d4dc3096ca86291e19f0284d0592a6ee89ec257204075a985d00f4788d816a89c41d0c1e0c8d281c480 + checksum: 10/842d8973cb34fa6d7535092e17e6d22c2642af1d9020aa06936cd1e9d6970830a888f9abf8907f60408c0a684b476b3b74316938d1edbad215881f9743f3940a languageName: node linkType: hard @@ -6610,7 +6408,7 @@ __metadata: languageName: node linkType: hard -"@types/http-proxy@npm:^1.17.15": +"@types/http-proxy@npm:^1.17.15, @types/http-proxy@npm:^1.17.8": version: 1.17.16 resolution: "@types/http-proxy@npm:1.17.16" dependencies: @@ -6619,15 +6417,6 @@ __metadata: languageName: node linkType: hard -"@types/http-proxy@npm:^1.17.8": - version: 1.17.14 - resolution: "@types/http-proxy@npm:1.17.14" - dependencies: - "@types/node": "npm:*" - checksum: 10/aa1a3e66cd43cbf06ea5901bf761d2031200a0ab42ba7e462a15c752e70f8669f21fb3be7c2f18fefcb83b95132dfa15740282e7421b856745598fbaea8e3a42 - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -6801,11 +6590,11 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>=10.0.0": - version: 22.15.3 - resolution: "@types/node@npm:22.15.3" + version: 24.0.10 + resolution: "@types/node@npm:24.0.10" dependencies: - undici-types: "npm:~6.21.0" - checksum: 10/6b4ff03c36598432b419980f828281aa16383e2de6eb61f73275495ef8d2cbf8cb5607659b4cae5ff8b2b2ff69913ea07ffcc0be029e4280b6e8bc138dc6629b + undici-types: "npm:~7.8.0" + checksum: 10/ff8921c515d72fbc0a11ff282096e2d2e11ac04a2e9c7f765bcec5cb69cd367a88ab5dd556dc162ac98b9212957939b7ae80f12f3fc90db10c82135affd6d120 languageName: node linkType: hard @@ -7497,7 +7286,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.14.0": +"acorn@npm:^8.0.0, acorn@npm:^8.14.0, acorn@npm:^8.8.2": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -7506,15 +7295,6 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.8.2": - version: 8.11.3 - resolution: "acorn@npm:8.11.3" - bin: - acorn: bin/acorn - checksum: 10/b688e7e3c64d9bfb17b596e1b35e4da9d50553713b3b3630cf5690f2b023a84eac90c56851e6912b483fe60e8b4ea28b254c07e92f17ef83d72d78745a8352dd - languageName: node - linkType: hard - "add-stream@npm:^1.0.0": version: 1.0.0 resolution: "add-stream@npm:1.0.0" @@ -7899,7 +7679,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.10.0": +"axios@npm:^1.10.0, axios@npm:^1.8.3": version: 1.10.0 resolution: "axios@npm:1.10.0" dependencies: @@ -7910,17 +7690,6 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.8.3": - version: 1.9.0 - resolution: "axios@npm:1.9.0" - dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10/a2f90bba56820883879f32a237e2b9ff25c250365dcafd41cec41b3406a3df334a148f90010182dfdadb4b41dc59f6f0b3e8898ff41b666d1157b5f3f4523497 - languageName: node - linkType: hard - "babel-jest@npm:30.0.4": version: 30.0.4 resolution: "babel-jest@npm:30.0.4" @@ -8240,16 +8009,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.2, braces@npm:~3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" - dependencies: - fill-range: "npm:^7.0.1" - checksum: 10/966b1fb48d193b9d155f810e5efd1790962f2c4e0829f8440b8ad236ba009222c501f70185ef732fef17a4c490bb33a03b90dab0631feafbdf447da91e8165b1 - languageName: node - linkType: hard - -"braces@npm:^3.0.3": +"braces@npm:^3.0.2, braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -8485,7 +8245,7 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.2": +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: @@ -8495,16 +8255,6 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.1": - version: 1.0.1 - resolution: "call-bind-apply-helpers@npm:1.0.1" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - checksum: 10/6e30c621170e45f1fd6735e84d02ee8e02a3ab95cb109499d5308cbe5d1e84d0cd0e10b48cc43c76aa61450ae1b03a7f89c37c10fc0de8d4998b42aab0f268cc - languageName: node - linkType: hard - "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.7, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" @@ -8927,10 +8677,10 @@ __metadata: languageName: node linkType: hard -"code-block-writer@npm:^13.0.1": - version: 13.0.1 - resolution: "code-block-writer@npm:13.0.1" - checksum: 10/3da803b1149d05a09b99e150df0e6d2ac5007bcf2ddd23d72e8b3e827cb6b7cb69b695472cfbc8b46a2bca4e7c11636788b9a7e7d518f3b45d0bddcac240b4af +"code-block-writer@npm:^13.0.3": + version: 13.0.3 + resolution: "code-block-writer@npm:13.0.3" + checksum: 10/771546224f38610eecee0598e83c9e0f86dcd600ea316dbf27c2cfebaab4fed51b042325aa460b8e0f131fac5c1de208f6610a1ddbffe4b22e76f9b5256707cb languageName: node linkType: hard @@ -10058,39 +9808,27 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": - version: 4.3.7 - resolution: "debug@npm:4.3.7" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a - languageName: node - linkType: hard - -"debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0, debug@npm:^4.4.1": + version: 4.4.1 + resolution: "debug@npm:4.4.1" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe languageName: node linkType: hard -"debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:~4.3.1, debug@npm:~4.3.2, debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/8e2709b2144f03c7950f8804d01ccb3786373df01e406a0f66928e47001cf2d336cbed9ee137261d4f90d68d8679468c755e3548ed83ddacdc82b194d2468afe + checksum: 10/71168908b9a78227ab29d5d25fe03c5867750e31ce24bf2c44a86efc5af041758bb56569b0a3d48a9b5344c00a24a777e6f4100ed6dfd9534a42c1dde285125a languageName: node linkType: hard @@ -10495,7 +10233,7 @@ __metadata: languageName: node linkType: hard -"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1": +"dunder-proto@npm:^1.0.1": version: 1.0.1 resolution: "dunder-proto@npm:1.0.1" dependencies: @@ -10851,7 +10589,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:0.25.5": +"esbuild@npm:0.25.5, esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": version: 0.25.5 resolution: "esbuild@npm:0.25.5" dependencies: @@ -10937,92 +10675,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.25.0, esbuild@npm:~0.25.0": - version: 0.25.3 - resolution: "esbuild@npm:0.25.3" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.3" - "@esbuild/android-arm": "npm:0.25.3" - "@esbuild/android-arm64": "npm:0.25.3" - "@esbuild/android-x64": "npm:0.25.3" - "@esbuild/darwin-arm64": "npm:0.25.3" - "@esbuild/darwin-x64": "npm:0.25.3" - "@esbuild/freebsd-arm64": "npm:0.25.3" - "@esbuild/freebsd-x64": "npm:0.25.3" - "@esbuild/linux-arm": "npm:0.25.3" - "@esbuild/linux-arm64": "npm:0.25.3" - "@esbuild/linux-ia32": "npm:0.25.3" - "@esbuild/linux-loong64": "npm:0.25.3" - "@esbuild/linux-mips64el": "npm:0.25.3" - "@esbuild/linux-ppc64": "npm:0.25.3" - "@esbuild/linux-riscv64": "npm:0.25.3" - "@esbuild/linux-s390x": "npm:0.25.3" - "@esbuild/linux-x64": "npm:0.25.3" - "@esbuild/netbsd-arm64": "npm:0.25.3" - "@esbuild/netbsd-x64": "npm:0.25.3" - "@esbuild/openbsd-arm64": "npm:0.25.3" - "@esbuild/openbsd-x64": "npm:0.25.3" - "@esbuild/sunos-x64": "npm:0.25.3" - "@esbuild/win32-arm64": "npm:0.25.3" - "@esbuild/win32-ia32": "npm:0.25.3" - "@esbuild/win32-x64": "npm:0.25.3" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10/f1ff72289938330312926421f90eea442025cbbac295a7a2e8cfc2abbd9e3a8bc1502883468b0487e4020f1369e4726c851a2fa4b65a7c71331940072c3a1808 - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -11504,7 +11156,7 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.3": +"fdir@npm:^6.4.3, fdir@npm:^6.4.4": version: 6.4.6 resolution: "fdir@npm:6.4.6" peerDependencies: @@ -11516,18 +11168,6 @@ __metadata: languageName: node linkType: hard -"fdir@npm:^6.4.4": - version: 6.4.5 - resolution: "fdir@npm:6.4.5" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10/8f5a2107fe0486f61af9a0666f2b7c62a229c738330e22ff8795bfbaabcf2294fb79460b73830b8824fc6eef91e21f676bac66ca982d5ee7e92ee9b68c07775f - languageName: node - linkType: hard - "figures@npm:3.2.0, figures@npm:^3.0.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -11577,15 +11217,6 @@ __metadata: languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" - dependencies: - to-regex-range: "npm:^5.0.1" - checksum: 10/e260f7592fd196b4421504d3597cc76f4a1ca7a9488260d533b611fc3cefd61e9a9be1417cb82d3b01ad9f9c0ff2dbf258e1026d2445e26b0cf5148ff4250429 - languageName: node - linkType: hard - "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -11901,7 +11532,7 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.3.0": +"get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.3.0": version: 1.3.0 resolution: "get-intrinsic@npm:1.3.0" dependencies: @@ -11919,24 +11550,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": - version: 1.2.6 - resolution: "get-intrinsic@npm:1.2.6" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - dunder-proto: "npm:^1.0.0" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - function-bind: "npm:^1.1.2" - gopd: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - math-intrinsics: "npm:^1.0.0" - checksum: 10/a1ffae6d7893a6fa0f4d1472adbc85095edd6b3b0943ead97c3738539cecb19d422ff4d48009eed8c3c27ad678c2b1e38a83b1a1e96b691d13ed8ecefca1068d - languageName: node - linkType: hard - "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -13586,7 +13199,7 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:6.0.3, istanbul-lib-instrument@npm:^6.0.2": +"istanbul-lib-instrument@npm:6.0.3, istanbul-lib-instrument@npm:^6.0.0, istanbul-lib-instrument@npm:^6.0.2": version: 6.0.3 resolution: "istanbul-lib-instrument@npm:6.0.3" dependencies: @@ -13612,19 +13225,6 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^6.0.0": - version: 6.0.2 - resolution: "istanbul-lib-instrument@npm:6.0.2" - dependencies: - "@babel/core": "npm:^7.23.9" - "@babel/parser": "npm:^7.23.9" - "@istanbuljs/schema": "npm:^0.1.3" - istanbul-lib-coverage: "npm:^3.2.0" - semver: "npm:^7.5.4" - checksum: 10/3aee19be199350182827679a137e1df142a306e9d7e20bb5badfd92ecc9023a7d366bc68e7c66e36983654a02a67401d75d8debf29fc6d4b83670fde69a594fc - languageName: node - linkType: hard - "istanbul-lib-report@npm:^3.0.0": version: 3.0.1 resolution: "istanbul-lib-report@npm:3.0.1" @@ -14467,18 +14067,7 @@ __metadata: languageName: node linkType: hard -"katex@npm:^0.16.0, katex@npm:^0.16.9": - version: 0.16.18 - resolution: "katex@npm:0.16.18" - dependencies: - commander: "npm:^8.3.0" - bin: - katex: cli.js - checksum: 10/5c90a770969eaaa18cb62e5a2f4937704e4378e6c5848e02138b3a681bc4bda5faf3fba33a12642f042345c429fafb4af48c2e36ecbe953f7db2cacb7cedc28f - languageName: node - linkType: hard - -"katex@npm:^0.16.21": +"katex@npm:^0.16.0, katex@npm:^0.16.21, katex@npm:^0.16.9": version: 0.16.22 resolution: "katex@npm:0.16.22" dependencies: @@ -15286,7 +14875,7 @@ __metadata: languageName: node linkType: hard -"math-intrinsics@npm:^1.0.0, math-intrinsics@npm:^1.1.0": +"math-intrinsics@npm:^1.1.0": version: 1.1.0 resolution: "math-intrinsics@npm:1.1.0" checksum: 10/11df2eda46d092a6035479632e1ec865b8134bdfc4bd9e571a656f4191525404f13a283a515938c3a8de934dbfd9c09674d9da9fa831e6eb7e22b50b197d2edd @@ -16298,6 +15887,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.0.1": + version: 10.0.3 + resolution: "minimatch@npm:10.0.3" + dependencies: + "@isaacs/brace-expansion": "npm:^5.0.0" + checksum: 10/d5b8b2538b367f2cfd4aeef27539fddeee58d1efb692102b848e4a968a09780a302c530eb5aacfa8c57f7299155fb4b4e85219ad82664dcef5c66f657111d9b8 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -16334,15 +15932,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.3": - version: 9.0.4 - resolution: "minimatch@npm:9.0.4" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/4cdc18d112b164084513e890d6323370db14c22249d536ad1854539577a895e690a27513dc346392f61a4a50afbbd8abc88f3f25558bfbbbb862cd56508b20f5 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -17192,7 +16781,7 @@ __metadata: languageName: node linkType: hard -"npm-package-arg@npm:12.0.2": +"npm-package-arg@npm:12.0.2, npm-package-arg@npm:^12.0.0": version: 12.0.2 resolution: "npm-package-arg@npm:12.0.2" dependencies: @@ -17216,18 +16805,6 @@ __metadata: languageName: node linkType: hard -"npm-package-arg@npm:^12.0.0": - version: 12.0.1 - resolution: "npm-package-arg@npm:12.0.1" - dependencies: - hosted-git-info: "npm:^8.0.0" - proc-log: "npm:^5.0.0" - semver: "npm:^7.3.5" - validate-npm-package-name: "npm:^6.0.0" - checksum: 10/63ef48708653aa1a5e4353116cbbe78be3630dcd92a12a559cf9c9a8b71bbaf5016b8c99ab020730587772cc1892fa924bb2ac3baef1c740847b130c56ef531d - languageName: node - linkType: hard - "npm-packlist@npm:8.0.2, npm-packlist@npm:^8.0.0": version: 8.0.2 resolution: "npm-packlist@npm:8.0.2" @@ -18687,16 +18264,7 @@ __metadata: languageName: node linkType: hard -"qs@npm:^6.11.0, qs@npm:^6.5.2": - version: 6.13.1 - resolution: "qs@npm:6.13.1" - dependencies: - side-channel: "npm:^1.0.6" - checksum: 10/53cf5fdc5f342a9ffd3968f20c8c61624924cf928d86fff525240620faba8ca5cfd6c3f12718cc755561bfc3dc9721bc8924e38f53d8925b03940f0b8a902212 - languageName: node - linkType: hard - -"qs@npm:^6.12.3, qs@npm:^6.14.0": +"qs@npm:^6.11.0, qs@npm:^6.12.3, qs@npm:^6.14.0, qs@npm:^6.5.2": version: 6.14.0 resolution: "qs@npm:6.14.0" dependencies: @@ -20066,19 +19634,7 @@ __metadata: languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": - version: 2.4.11 - resolution: "sha.js@npm:2.4.11" - dependencies: - inherits: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - bin: - sha.js: ./bin.js - checksum: 10/d833bfa3e0a67579a6ce6e1bc95571f05246e0a441dd8c76e3057972f2a3e098465687a4369b07e83a0375a88703577f71b5b2e966809e67ebc340dbedb478c7 - languageName: node - linkType: hard - -"sha.js@npm:^2.4.11": +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8": version: 2.4.12 resolution: "sha.js@npm:2.4.12" dependencies: @@ -21309,13 +20865,13 @@ __metadata: languageName: node linkType: hard -"ts-morph@npm:^22.0.0": - version: 22.0.0 - resolution: "ts-morph@npm:22.0.0" +"ts-morph@npm:^26.0.0": + version: 26.0.0 + resolution: "ts-morph@npm:26.0.0" dependencies: - "@ts-morph/common": "npm:~0.23.0" - code-block-writer: "npm:^13.0.1" - checksum: 10/e5d81d0d8d990fa9f86e285bd4052bcfa462e2f798f7eda86e11afc7d884dfdb053998dcbf79942942e8032070f8b266745e017771674a169731494fe035e192 + "@ts-morph/common": "npm:~0.27.0" + code-block-writer: "npm:^13.0.3" + checksum: 10/0b76beec9f9641bf3304de8f41327db4e563729b4e13b37b546a4231a4310cb760cad0c88fe8d11235bbe6f9de0471dda0992b8c35535a4e4c34033304f22d14 languageName: node linkType: hard @@ -21330,20 +20886,13 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.8.1, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:2.8.1, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.3.0, tslib@npm:^2.4.0": - version: 2.7.0 - resolution: "tslib@npm:2.7.0" - checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 - languageName: node - linkType: hard - "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" @@ -21656,6 +21205,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~7.8.0": + version: 7.8.0 + resolution: "undici-types@npm:7.8.0" + checksum: 10/fcff3fbab234f067fbd69e374ee2c198ba74c364ceaf6d93db7ca267e784457b5518cd01d0d2329b075f412574205ea3172a9a675facb49b4c9efb7141cd80b7 + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.1 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1" From 36635f0c3379f308a36471cb4efea61e6565c42e Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 13:58:14 +0100 Subject: [PATCH 11/15] fix: sort imports --- .../typescript-nextjs/typescript-nextjs-app-router-builder.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts index 6f87642c3..9b476fbfc 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts @@ -132,7 +132,9 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { .forEach((it) => it.remove()) this.sourceFile.addImportDeclaration({ - namedImports: Array.from(this.httpMethodsUsed).map((it) => `_${it}`), + namedImports: Array.from(this.httpMethodsUsed) + .map((it) => `_${it}`) + .sort(), moduleSpecifier: from, }) From 197b3761df705d350b61797bb3aacf145d0ec4fa Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 14:15:14 +0100 Subject: [PATCH 12/15] feat: nextjs runtime package --- .../typescript-nextjs/package.json | 1 + .../todo-lists.yaml/attachments/route.ts | 37 +++++---- .../list/[listId]/items/route.ts | 41 +++++----- .../todo-lists.yaml/list/[listId]/route.ts | 73 +++++++++-------- .../generated/todo-lists.yaml/list/route.ts | 25 +++--- .../typescript-nextjs-router-builder.ts | 31 ++++--- packages/typescript-nextjs-runtime/README.md | 10 +++ .../typescript-nextjs-runtime/jest.config.js | 12 +++ .../typescript-nextjs-runtime/package.json | 82 +++++++++++++++++++ .../typescript-nextjs-runtime/src/errors.ts | 50 +++++++++++ packages/typescript-nextjs-runtime/src/joi.ts | 80 ++++++++++++++++++ .../typescript-nextjs-runtime/src/server.ts | 62 ++++++++++++++ packages/typescript-nextjs-runtime/src/zod.ts | 56 +++++++++++++ .../typescript-nextjs-runtime/tsconfig.json | 9 ++ tsconfig.json | 3 +- yarn.lock | 21 +++++ 16 files changed, 489 insertions(+), 104 deletions(-) create mode 100644 packages/typescript-nextjs-runtime/README.md create mode 100644 packages/typescript-nextjs-runtime/jest.config.js create mode 100644 packages/typescript-nextjs-runtime/package.json create mode 100644 packages/typescript-nextjs-runtime/src/errors.ts create mode 100644 packages/typescript-nextjs-runtime/src/joi.ts create mode 100644 packages/typescript-nextjs-runtime/src/server.ts create mode 100644 packages/typescript-nextjs-runtime/src/zod.ts create mode 100644 packages/typescript-nextjs-runtime/tsconfig.json diff --git a/integration-tests/typescript-nextjs/package.json b/integration-tests/typescript-nextjs/package.json index e54039bad..8a24950c8 100644 --- a/integration-tests/typescript-nextjs/package.json +++ b/integration-tests/typescript-nextjs/package.json @@ -11,6 +11,7 @@ "validate": "tsc -p ./tsconfig.json" }, "dependencies": { + "@nahkies/typescript-nextjs-runtime": "*", "next": "^15.3.5", "react": "^19.1.0", "react-dom": "^19.1.0" diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts index 184c1a860..d0636f656 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -4,37 +4,38 @@ import {t_UnknownObject, t_UploadAttachmentBodySchema} from "../models" import { - KoaRuntimeError, + OpenAPIRuntimeError, RequestInputType, -} from "@nahkies/typescript-koa-runtime/errors" +} from "@nahkies/typescript-nextjs-runtime/errors" import { - KoaRuntimeResponder, - KoaRuntimeResponse, + OpenAPIRuntimeResponder, + OpenAPIRuntimeResponse, + Params, StatusCode, -} from "@nahkies/typescript-koa-runtime/server" -import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +} from "@nahkies/typescript-nextjs-runtime/server" +import {parseRequestInput} from "@nahkies/typescript-nextjs-runtime/zod" import {NextRequest} from "next/server" import {z} from "zod" // /attachments export type ListAttachmentsResponder = { - with200(): KoaRuntimeResponse -} & KoaRuntimeResponder + with200(): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type ListAttachments = ( respond: ListAttachmentsResponder, request: NextRequest, -) => Promise> +) => Promise> export type UploadAttachmentResponder = { - with202(): KoaRuntimeResponse -} & KoaRuntimeResponder + with202(): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type UploadAttachment = ( params: Params, respond: UploadAttachmentResponder, request: NextRequest, -) => Promise> +) => Promise> export const _GET = (implementation: ListAttachments) => @@ -49,17 +50,17 @@ export const _GET = const responder = { with200() { - return new KoaRuntimeResponse(200) + return new OpenAPIRuntimeResponse(200) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined @@ -86,17 +87,17 @@ export const _POST = const responder = { with202() { - return new KoaRuntimeResponse(202) + return new OpenAPIRuntimeResponse(202) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts index 3c2393ce0..1e15413f1 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts @@ -8,42 +8,43 @@ import { t_GetTodoListItemsParamSchema, } from "../../../models" import { - KoaRuntimeError, + OpenAPIRuntimeError, RequestInputType, -} from "@nahkies/typescript-koa-runtime/errors" +} from "@nahkies/typescript-nextjs-runtime/errors" import { - KoaRuntimeResponder, - KoaRuntimeResponse, + OpenAPIRuntimeResponder, + OpenAPIRuntimeResponse, + Params, StatusCode, StatusCode5xx, -} from "@nahkies/typescript-koa-runtime/server" -import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +} from "@nahkies/typescript-nextjs-runtime/server" +import {parseRequestInput} from "@nahkies/typescript-nextjs-runtime/zod" import {NextRequest} from "next/server" import {z} from "zod" // /list/{listId}/items export type GetTodoListItemsResponder = { - with200(): KoaRuntimeResponse<{ + with200(): OpenAPIRuntimeResponse<{ completedAt?: string content: string createdAt: string id: string }> - withStatusCode5xx(status: StatusCode5xx): KoaRuntimeResponse<{ + withStatusCode5xx(status: StatusCode5xx): OpenAPIRuntimeResponse<{ code: string message: string }> -} & KoaRuntimeResponder +} & OpenAPIRuntimeResponder export type GetTodoListItems = ( params: Params, respond: GetTodoListItemsResponder, request: NextRequest, -) => Promise> +) => Promise> export type CreateTodoListItemResponder = { - with204(): KoaRuntimeResponse -} & KoaRuntimeResponder + with204(): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type CreateTodoListItem = ( params: Params< @@ -54,7 +55,7 @@ export type CreateTodoListItem = ( >, respond: CreateTodoListItemResponder, request: NextRequest, -) => Promise> +) => Promise> const getTodoListItemsParamSchema = z.object({listId: z.string()}) @@ -78,7 +79,7 @@ export const _GET = const responder = { with200() { - return new KoaRuntimeResponse<{ + return new OpenAPIRuntimeResponse<{ completedAt?: string content: string createdAt: string @@ -86,20 +87,20 @@ export const _GET = }>(200) }, withStatusCode5xx(status: StatusCode5xx) { - return new KoaRuntimeResponse<{ + return new OpenAPIRuntimeResponse<{ code: string message: string }>(status) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined @@ -139,17 +140,17 @@ export const _POST = const responder = { with204() { - return new KoaRuntimeResponse(204) + return new OpenAPIRuntimeResponse(204) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts index a814e4e35..5fed57e82 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts @@ -12,37 +12,38 @@ import { } from "../../models" import {s_CreateUpdateTodoList} from "../../schemas" import { - KoaRuntimeError, + OpenAPIRuntimeError, RequestInputType, -} from "@nahkies/typescript-koa-runtime/errors" +} from "@nahkies/typescript-nextjs-runtime/errors" import { - KoaRuntimeResponder, - KoaRuntimeResponse, + OpenAPIRuntimeResponder, + OpenAPIRuntimeResponse, + Params, StatusCode, StatusCode4xx, -} from "@nahkies/typescript-koa-runtime/server" -import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +} from "@nahkies/typescript-nextjs-runtime/server" +import {parseRequestInput} from "@nahkies/typescript-nextjs-runtime/zod" import {NextRequest} from "next/server" import {z} from "zod" // /list/{listId} export type GetTodoListByIdResponder = { - with200(): KoaRuntimeResponse - withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse - withDefault(status: StatusCode): KoaRuntimeResponse -} & KoaRuntimeResponder + with200(): OpenAPIRuntimeResponse + withStatusCode4xx(status: StatusCode4xx): OpenAPIRuntimeResponse + withDefault(status: StatusCode): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type GetTodoListById = ( params: Params, respond: GetTodoListByIdResponder, request: NextRequest, -) => Promise> +) => Promise> export type UpdateTodoListByIdResponder = { - with200(): KoaRuntimeResponse - withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse - withDefault(status: StatusCode): KoaRuntimeResponse -} & KoaRuntimeResponder + with200(): OpenAPIRuntimeResponse + withStatusCode4xx(status: StatusCode4xx): OpenAPIRuntimeResponse + withDefault(status: StatusCode): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type UpdateTodoListById = ( params: Params< @@ -53,19 +54,19 @@ export type UpdateTodoListById = ( >, respond: UpdateTodoListByIdResponder, request: NextRequest, -) => Promise> +) => Promise> export type DeleteTodoListByIdResponder = { - with204(): KoaRuntimeResponse - withStatusCode4xx(status: StatusCode4xx): KoaRuntimeResponse - withDefault(status: StatusCode): KoaRuntimeResponse -} & KoaRuntimeResponder + with204(): OpenAPIRuntimeResponse + withStatusCode4xx(status: StatusCode4xx): OpenAPIRuntimeResponse + withDefault(status: StatusCode): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type DeleteTodoListById = ( params: Params, respond: DeleteTodoListByIdResponder, request: NextRequest, -) => Promise> +) => Promise> const getTodoListByIdParamSchema = z.object({listId: z.string()}) @@ -89,23 +90,23 @@ export const _GET = const responder = { with200() { - return new KoaRuntimeResponse(200) + return new OpenAPIRuntimeResponse(200) }, withStatusCode4xx(status: StatusCode4xx) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, withDefault(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined @@ -141,23 +142,23 @@ export const _PUT = const responder = { with200() { - return new KoaRuntimeResponse(200) + return new OpenAPIRuntimeResponse(200) }, withStatusCode4xx(status: StatusCode4xx) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, withDefault(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined @@ -187,23 +188,23 @@ export const _DELETE = const responder = { with204() { - return new KoaRuntimeResponse(204) + return new OpenAPIRuntimeResponse(204) }, withStatusCode4xx(status: StatusCode4xx) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, withDefault(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts index fcc18aa45..40d6da0b7 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -5,28 +5,29 @@ import {t_GetTodoListsQuerySchema, t_TodoList} from "../models" import {s_Statuses} from "../schemas" import { - KoaRuntimeError, + OpenAPIRuntimeError, RequestInputType, -} from "@nahkies/typescript-koa-runtime/errors" +} from "@nahkies/typescript-nextjs-runtime/errors" import { - KoaRuntimeResponder, - KoaRuntimeResponse, + OpenAPIRuntimeResponder, + OpenAPIRuntimeResponse, + Params, StatusCode, -} from "@nahkies/typescript-koa-runtime/server" -import {Params, parseRequestInput} from "@nahkies/typescript-koa-runtime/zod" +} from "@nahkies/typescript-nextjs-runtime/server" +import {parseRequestInput} from "@nahkies/typescript-nextjs-runtime/zod" import {NextRequest} from "next/server" import {z} from "zod" // /list export type GetTodoListsResponder = { - with200(): KoaRuntimeResponse -} & KoaRuntimeResponder + with200(): OpenAPIRuntimeResponse +} & OpenAPIRuntimeResponder export type GetTodoLists = ( params: Params, respond: GetTodoListsResponder, request: NextRequest, -) => Promise> +) => Promise> const getTodoListsQuerySchema = z.object({ created: z.string().datetime({offset: true}).optional(), @@ -61,17 +62,17 @@ export const _GET = const responder = { with200() { - return new KoaRuntimeResponse(200) + return new OpenAPIRuntimeResponse(200) }, withStatus(status: StatusCode) { - return new KoaRuntimeResponse(status) + return new OpenAPIRuntimeResponse(status) }, } const {status, body} = await implementation(input, responder, request) .then((it) => it.unpack()) .catch((err) => { - throw KoaRuntimeError.HandlerError(err) + throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts index aaacdce87..f05e79b2e 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts @@ -32,14 +32,12 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { } protected buildImports(): void { - // todo: unsure why, but adding an export at `.` of index.ts doesn't work properly this.imports - .from("@nahkies/typescript-koa-runtime/server") + .from("@nahkies/typescript-nextjs-runtime/server") .add( - "startServer", - "ServerConfig", - "KoaRuntimeResponse", - "KoaRuntimeResponder", + "OpenAPIRuntimeResponse", + "OpenAPIRuntimeResponder", + "Params", "StatusCode2xx", "StatusCode3xx", "StatusCode4xx", @@ -50,17 +48,17 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { this.imports.from("next/server").add("NextRequest", "NextResponse") this.imports - .from("@nahkies/typescript-koa-runtime/errors") - .add("KoaRuntimeError", "RequestInputType") + .from("@nahkies/typescript-nextjs-runtime/errors") + .add("OpenAPIRuntimeError", "RequestInputType") if (this.schemaBuilder instanceof ZodBuilder) { this.imports - .from("@nahkies/typescript-koa-runtime/zod") - .add("parseRequestInput", "Params", "responseValidationFactory") + .from("@nahkies/typescript-nextjs-runtime/zod") + .add("parseRequestInput", "responseValidationFactory") } else if (this.schemaBuilder instanceof JoiBuilder) { this.imports - .from("@nahkies/typescript-koa-runtime/joi") - .add("parseRequestInput", "Params", "responseValidationFactory") + .from("@nahkies/typescript-nextjs-runtime/joi") + .add("parseRequestInput", "responseValidationFactory") } } @@ -89,9 +87,8 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { const responseSchemas = builder.responseSchemas() const responder = builder.responder( - // TODO: nextjs types - "KoaRuntimeResponder", - "KoaRuntimeResponse", + "OpenAPIRuntimeResponder", + "OpenAPIRuntimeResponse", ) this.operationTypes.push({ @@ -110,7 +107,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { "request: NextRequest", ] .filter(isDefined) - .join(",")}) => Promise>`, + .join(",")}) => Promise>`, kind: "type", }), ], @@ -152,7 +149,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { body, } = await implementation(${[params.hasParams ? "input" : undefined, "responder", "request"].filter(isDefined).join(",")}) .then(it => it.unpack()) - .catch(err => { throw KoaRuntimeError.HandlerError(err) }) + .catch(err => { throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) }`, diff --git a/packages/typescript-nextjs-runtime/README.md b/packages/typescript-nextjs-runtime/README.md new file mode 100644 index 000000000..483c37be5 --- /dev/null +++ b/packages/typescript-nextjs-runtime/README.md @@ -0,0 +1,10 @@ +# @nahkies/typescript-nextjs-runtime + +[![CI/CD](https://github.com/mnahkies/openapi-code-generator/actions/workflows/ci.yml/badge.svg)](https://github.com/mnahkies/openapi-code-generator/actions?query=branch%3Amain+event%3Apush) +[![npm](https://img.shields.io/npm/dm/%40nahkies%2Ftypescript-nextjs-runtime.svg)](https://www.npmjs.com/package/@nahkies/typescript-nextjs-runtime) + +This is a supporting package for code generated using [@nahkies/openapi-code-generator](https://www.npmjs.com/package/@nahkies/openapi-code-generator) using the Typescript NextJS server stubs template. + +You can [read the docs](https://openapi-code-generator.nahkies.co.nz/guides/server-templates/typescript-nextjs) to find out more! + +It's not intended by be used standalone. Similar in spirit to [tslib](https://www.npmjs.com/package/tslib) diff --git a/packages/typescript-nextjs-runtime/jest.config.js b/packages/typescript-nextjs-runtime/jest.config.js new file mode 100644 index 000000000..e4604b03a --- /dev/null +++ b/packages/typescript-nextjs-runtime/jest.config.js @@ -0,0 +1,12 @@ +const base = require("../../jest.base") +const {name: displayName} = require("./package.json") + +/** + * @type { import('@jest/types').Config.ProjectConfig } + */ +const config = { + ...base, + displayName, +} + +module.exports = config diff --git a/packages/typescript-nextjs-runtime/package.json b/packages/typescript-nextjs-runtime/package.json new file mode 100644 index 000000000..b4c35f7e4 --- /dev/null +++ b/packages/typescript-nextjs-runtime/package.json @@ -0,0 +1,82 @@ +{ + "name": "@nahkies/typescript-nextjs-runtime", + "version": "0.20.1", + "description": "Runtime package for code generated by @nahkies/openapi-code-generator using the typescript-nextjs template", + "license": "MIT", + "author": { + "name": "Michael Nahkies", + "email": "support@nahkies.co.nz" + }, + "homepage": "https://openapi-code-generator.nahkies.co.nz/", + "repository": { + "type": "git", + "url": "https://github.com/mnahkies/openapi-code-generator.git", + "directory": "packages/typescript-nextjs-runtime" + }, + "bugs": { + "url": "https://github.com/mnahkies/openapi-code-generator/issues" + }, + "exports": { + "./errors": { + "require": "./dist/errors.js", + "import": "./dist/errors.js", + "types": "./dist/errors.d.ts" + }, + "./server": { + "require": "./dist/server.js", + "import": "./dist/server.js", + "types": "./dist/server.d.ts" + }, + "./joi": { + "require": "./dist/joi.js", + "import": "./dist/joi.js", + "types": "./dist/joi.d.ts" + }, + "./zod": { + "require": "./dist/zod.js", + "import": "./dist/zod.js", + "types": "./dist/zod.d.ts" + } + }, + "scripts": { + "clean": "rm -rf ./dist && rm tsconfig.tsbuildinfo", + "build": "tsc -p ./tsconfig.json", + "test": "jest" + }, + "peerDependencies": { + "joi": "^17.1.1", + "next": "^15.3.5", + "zod": "^3.20.6" + }, + "peerDependenciesMeta": { + "joi": { + "optional": true + }, + "zod": { + "optional": true + } + }, + "devDependencies": { + "jest": "^30.0.4", + "joi": "^17.13.3", + "typescript": "~5.8.3", + "zod": "^3.25.74" + }, + "files": [ + "src", + "dist", + "README.md", + "CHANGELOG.md", + "tsconfig.json" + ], + "keywords": [ + "@nahkies/openapi-code-generator", + "runtime", + "typescript-nextjs", + "nextjs", + "zod" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/typescript-nextjs-runtime/src/errors.ts b/packages/typescript-nextjs-runtime/src/errors.ts new file mode 100644 index 000000000..4410ef1d5 --- /dev/null +++ b/packages/typescript-nextjs-runtime/src/errors.ts @@ -0,0 +1,50 @@ +export enum RequestInputType { + RouteParam = "route params", + QueryString = "querystring", + RequestBody = "request body", + RequestHeader = "request header", +} + +export class OpenAPIRuntimeError extends Error { + private constructor( + message: string, + cause: unknown, + public readonly phase: + | "request_validation" + | "request_handler" + | "response_validation", + ) { + super(message, {cause}) + } + + static RequestError( + cause: unknown, + inputType: RequestInputType, + ): OpenAPIRuntimeError { + return new OpenAPIRuntimeError( + `Request validation failed parsing ${inputType}`, + cause, + "request_validation", + ) + } + + static HandlerError(cause: unknown) { + return new OpenAPIRuntimeError( + "Request handler threw unhandled exception", + cause, + "request_handler", + ) + } + + static ResponseError(cause: unknown) { + return new OpenAPIRuntimeError( + "Response body failed validation", + cause, + "response_validation", + ) + } + + static isOpenAPIError(err: unknown): err is OpenAPIRuntimeError { + return err instanceof OpenAPIRuntimeError + } +} diff --git a/packages/typescript-nextjs-runtime/src/joi.ts b/packages/typescript-nextjs-runtime/src/joi.ts new file mode 100644 index 000000000..8cfb06d91 --- /dev/null +++ b/packages/typescript-nextjs-runtime/src/joi.ts @@ -0,0 +1,80 @@ +import type {Schema as JoiSchema} from "joi" +import {OpenAPIRuntimeError, type RequestInputType} from "./errors" + +// Note: joi types don't appear to have an equivalent of z.infer, +// hence any seems about as good as we can do here. +export function parseRequestInput( + schema: Schema, + input: unknown, + type: RequestInputType, + // biome-ignore lint/suspicious/noExplicitAny: +): any +export function parseRequestInput( + schema: undefined, + input: unknown, + type: RequestInputType, +): undefined +export function parseRequestInput( + schema: Schema | undefined, + input: unknown, + type: RequestInputType, + // biome-ignore lint/suspicious/noExplicitAny: +): any { + try { + if (!schema) { + return undefined + } + + const result = schema.validate(input, {stripUnknown: true}) + + if (result.error) { + throw result.error + } + + return result.value + } catch (err) { + throw OpenAPIRuntimeError.RequestError(err, type) + } +} + +export function responseValidationFactory( + possibleResponses: [string, JoiSchema][], + defaultResponse?: JoiSchema, +) { + // Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx + possibleResponses.sort((x, y) => (x[0] < y[0] ? -1 : 1)) + + return (status: number, value: unknown) => { + try { + for (const [match, schema] of possibleResponses) { + const isMatch = + (/^\d+$/.test(match) && String(status) === match) || + (/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0]) + + if (isMatch) { + const result = schema.validate(value) + + if (result.error) { + throw result.error + } + + return result.value + } + } + + if (defaultResponse) { + const result = defaultResponse.validate(value) + + if (result.error) { + throw result.error + } + + return result.value + } + + return value + } catch (err) { + throw OpenAPIRuntimeError.ResponseError(err) + } + } +} diff --git a/packages/typescript-nextjs-runtime/src/server.ts b/packages/typescript-nextjs-runtime/src/server.ts new file mode 100644 index 000000000..3a9c01860 --- /dev/null +++ b/packages/typescript-nextjs-runtime/src/server.ts @@ -0,0 +1,62 @@ +// from https://stackoverflow.com/questions/39494689/is-it-possible-to-restrict-number-to-a-certain-range +type Enumerate< + N extends number, + Acc extends number[] = [], +> = Acc["length"] extends N + ? Acc[number] + : Enumerate + +type IntRange = F extends T + ? F + : Exclude, Enumerate> extends never + ? never + : Exclude, Enumerate> | T + +export type StatusCode1xx = IntRange<100, 199> // `1${number}${number}` +export type StatusCode2xx = IntRange<200, 299> // `2${number}${number}` +export type StatusCode3xx = IntRange<300, 399> // `3${number}${number}` +export type StatusCode4xx = IntRange<400, 499> // `4${number}${number}` +export type StatusCode5xx = IntRange<500, 599> // `5${number}${number}` +export type StatusCode = + | StatusCode1xx + | StatusCode2xx + | StatusCode3xx + | StatusCode4xx + | StatusCode5xx + +export type Response = { + status: Status + body: Type +} + +export const SkipResponse = Symbol("skip response processing") + +export class OpenAPIRuntimeResponse { + private _body?: Type + + constructor(private readonly status: StatusCode) {} + + body(body: Type): this { + this._body = body + return this + } + + unpack(): Response { + return {status: this.status, body: this._body} + } +} + +export type OpenAPIRuntimeResponder< + Status extends StatusCode = StatusCode, + // biome-ignore lint/suspicious/noExplicitAny: + Type = any, +> = { + withStatus: (status: Status) => OpenAPIRuntimeResponse +} + +export type Params = { + params: Params + query: Query + body: Body + headers: Header +} diff --git a/packages/typescript-nextjs-runtime/src/zod.ts b/packages/typescript-nextjs-runtime/src/zod.ts new file mode 100644 index 000000000..76dac8251 --- /dev/null +++ b/packages/typescript-nextjs-runtime/src/zod.ts @@ -0,0 +1,56 @@ +import type {z} from "zod" +import {OpenAPIRuntimeError, type RequestInputType} from "./errors" + +export function parseRequestInput( + schema: Schema, + input: unknown, + type: RequestInputType, +): z.infer +export function parseRequestInput( + schema: undefined, + input: unknown, + type: RequestInputType, +): undefined +export function parseRequestInput( + schema: Schema | undefined, + input: unknown, + type: RequestInputType, +): z.infer | undefined { + try { + return schema?.parse(input) + } catch (err) { + throw OpenAPIRuntimeError.RequestError(err, type) + } +} + +// TODO: optional response validation +export function responseValidationFactory( + possibleResponses: [string, z.ZodTypeAny][], + defaultResponse?: z.ZodTypeAny, +) { + // Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx + possibleResponses.sort((x, y) => (x[0] < y[0] ? -1 : 1)) + + return (status: number, value: unknown) => { + try { + for (const [match, schema] of possibleResponses) { + const isMatch = + (/^\d+$/.test(match) && String(status) === match) || + (/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0]) + + if (isMatch) { + return schema.parse(value) + } + } + + if (defaultResponse) { + return defaultResponse.parse(value) + } + + // TODO: throw on unmatched response + return value + } catch (err) { + throw OpenAPIRuntimeError.ResponseError(err) + } + } +} diff --git a/packages/typescript-nextjs-runtime/tsconfig.json b/packages/typescript-nextjs-runtime/tsconfig.json new file mode 100644 index 000000000..76e65dcfb --- /dev/null +++ b/packages/typescript-nextjs-runtime/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "references": [] +} diff --git a/tsconfig.json b/tsconfig.json index f3ac6ef0c..dc1e3f4d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ { "path": "./packages/typescript-axios-runtime" }, { "path": "./packages/typescript-express-runtime" }, { "path": "./packages/typescript-fetch-runtime" }, - { "path": "./packages/typescript-koa-runtime" } + { "path": "./packages/typescript-koa-runtime" }, + { "path": "./packages/typescript-nextjs-runtime" } ] } diff --git a/yarn.lock b/yarn.lock index b7b393316..f51f70b20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3813,6 +3813,26 @@ __metadata: languageName: unknown linkType: soft +"@nahkies/typescript-nextjs-runtime@npm:*, @nahkies/typescript-nextjs-runtime@workspace:packages/typescript-nextjs-runtime": + version: 0.0.0-use.local + resolution: "@nahkies/typescript-nextjs-runtime@workspace:packages/typescript-nextjs-runtime" + dependencies: + jest: "npm:^30.0.4" + joi: "npm:^17.13.3" + typescript: "npm:~5.8.3" + zod: "npm:^3.25.74" + peerDependencies: + joi: ^17.1.1 + next: ^15.3.5 + zod: ^3.20.6 + peerDependenciesMeta: + joi: + optional: true + zod: + optional: true + languageName: unknown + linkType: soft + "@napi-rs/nice-android-arm-eabi@npm:1.0.4": version: 1.0.4 resolution: "@napi-rs/nice-android-arm-eabi@npm:1.0.4" @@ -21143,6 +21163,7 @@ __metadata: version: 0.0.0-use.local resolution: "typescript-nextjs@workspace:integration-tests/typescript-nextjs" dependencies: + "@nahkies/typescript-nextjs-runtime": "npm:*" "@types/node": "npm:^22.16.0" "@types/react": "npm:^19.1.8" "@types/react-dom": "npm:^19.1.6" From 77aa9da7722b8442294a1fef38c5f94460bf49e5 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 15:12:01 +0100 Subject: [PATCH 13/15] feat: wip documentation --- .../app/todo-lists.yaml/attachments/route.ts | 4 +- .../list/[listId]/items/route.ts | 4 +- .../todo-lists.yaml/list/[listId]/route.ts | 6 +- .../src/app/todo-lists.yaml/list/route.ts | 2 +- .../typescript-nextjs/page.mdx | 173 ++++++++++++++++++ .../typescript-nextjs-app-router-builder.ts | 2 +- .../typescript-nextjs.generator.ts | 4 +- 7 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts index 17adde126..1941329f7 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts @@ -1,10 +1,10 @@ import {_GET, _POST} from "../../../generated/todo-lists.yaml/attachments/route" -export const GET = _GET(async (respond, context) => { +export const GET = _GET(async (respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const POST = _POST(async ({body}, respond, context) => { +export const POST = _POST(async ({body}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts index b3d877530..002c82e4b 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts @@ -3,11 +3,11 @@ import { _POST, } from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route" -export const GET = _GET(async ({params}, respond, context) => { +export const GET = _GET(async ({params}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const POST = _POST(async ({params, body}, respond, context) => { +export const POST = _POST(async ({params, body}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts index d9fa40af0..e305d93de 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts @@ -4,15 +4,15 @@ import { _PUT, } from "../../../../generated/todo-lists.yaml/list/[listId]/route" -export const GET = _GET(async ({params}, respond, context) => { +export const GET = _GET(async ({params}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const PUT = _PUT(async ({params, body}, respond, context) => { +export const PUT = _PUT(async ({params, body}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) -export const DELETE = _DELETE(async ({params}, respond, context) => { +export const DELETE = _DELETE(async ({params}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts index 84539220c..1eeb847f0 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts @@ -1,6 +1,6 @@ import {_GET} from "../../../generated/todo-lists.yaml/list/route" -export const GET = _GET(async ({query}, respond, context) => { +export const GET = _GET(async ({query}, respond, request) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) }) diff --git a/packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx b/packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx new file mode 100644 index 000000000..836fb5cf0 --- /dev/null +++ b/packages/documentation/src/app/guides/server-templates/typescript-nextjs/page.mdx @@ -0,0 +1,173 @@ +import {Tabs} from 'nextra/components' + +# Using the `typescript-nextjs` template + +> ⚠️ **Alpha Template** ⚠️ +> +> This template is currently in **alpha**. APIs and features are subject to change.
+> It might break in unexpected ways, or **mangle** your code. +> +> You can see an example of it used on a real project here:
+> https://github.com/mnahkies/spdx-dependency-track + + +The `typescript-nextjs` template outputs scaffolding code that handles the following: + +- Generates route handlers in the Next.js App Router (app/api/.../route.ts) for every operation in your OpenAPI spec +- Parses and validates request input (query, params, headers, and body) using `zod`, or `joi` +- Validates response types at runtime before sending them, ensuring they conform to your OpenAPI spec +- Enforces full type safety for each handler’s inputs and outputs +- Additionally, emits a [typescript-fetch](../client-templates/typescript-fetch) client for making requests to the routes from your react code + +See [integration-tests/typescript-nextjs](https://github.com/mnahkies/openapi-code-generator/tree/main/integration-tests/typescript-nextjs) for more samples. + +### Install dependencies +First install the CLI and the required runtime packages to your project: +```sh npm2yarn +npm i --dev @nahkies/openapi-code-generator +npm i @nahkies/typescript-nextjs-runtime next zod +``` + +See also [quick start](../../getting-started/quick-start) guide + +### Run generation + + + + ```sh npm2yarn + npm run openapi-code-generator \ + --input ./openapi.yaml \ + --input-type openapi3 \ + --output ./src \ + --template typescript-nextjs \ + --schema-builder zod + ``` + + + ```sh npm2yarn + npm run openapi-code-generator \ + --input ./typespec.tsp \ + --input-type typespec \ + --output ./src \ + --template typescript-nextjs \ + --schema-builder zod + ``` + + + + +### Using the generated code +Running the above will output a bunch of files into `./src`. Here's an example of the files output for a todo-list api specification: +```shell +src +├── app +│ ├── api +│ │ └── list +│ │ ├── [listId] +│ │ │ ├── items +│ │ │ │ └── route.ts +│ │ │ └── route.ts +│ │ └── route.ts +│ ├── layout.tsx +│ └── page.tsx +└── generated + ├── api + │ └── list + │ ├── [listId] + │ │ ├── items + │ │ │ └── route.ts + │ │ └── route.ts + │ └── route.ts + ├── client.ts + ├── models.ts + └── schemas.ts +```` + +`./src/app/../route.ts` +- a `route.ts` file is generated per operation, following Next.js App Router conventions +- exports handlers (GET, POST, etc.) for each HTTP method defined +- safe to edit, your route handler implementations go here +- calls into `./src/generated/...` for input/output validation logic + +`./src/generated/../route.ts` +- mirror structure of the `./src/app/.../route.ts` files +- contains glue code that parses input, validates responses, and calls your implementation + +`./src/generated/models.ts` +- exports plain TypeScript types for all schemas in your OpenAPI spec + +`./src/generated/schemas.ts` +- exports runtime schema validators (`zod` / `joi` depending on configuration) + +`./src/generated/client.ts` +- exports a [typescript-fetch](../client-templates/typescript-fetch) client for calling your API from frontend code +- see [use-with-react-query](../use-with-react-query) for integration with `react-query` + +#### Implementing routes + +Once generated usage should look something like this: + +```typescript +import {db} from "../../../../../db" +import { + _GET, + _POST, +} from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route" + +export const GET = _GET(async ({params}, respond, request) => { + const items = db.getTodoItems({listId: params.listId}) + + if (items) { + return respond.with200().body(items) + } + return respond + .with404() + .body({code: "not-found", message: `listId ${params.listId} not found`}) +}) + +export const POST = _POST(async ({params, body}, respond, request) => { + await db.insertTodoItem({ + listId: params.listId, + itemId: body.id, + content: body.content, + completedAt: body.completedAt, + }) + + return respond.with204() +}) + +``` + +#### Its safe to regenerate! +The template uses [ts-morph](https://ts-morph.com/) to **non-destructively generate and update** route.ts files. + +This means you can safely add your own logic to the scaffolded files, and future regenerations will preserve your +implementation code while updating the generated boilerplate. + +#### Error Handling + +> 🚧 Under construction +> +> Errors will be thrown for req/res validation issues, but currently its impossible to catch them. +> More thought is needed... + +### Escape Hatches + +> 🚧 Under construction +> +> The raw nextjs `request` object is passed to your implementation, however there is not yet +> a way to skip response processing. + +Most APIs won't need this, but in some cases (e.g. unsupported features), you can use escape hatches to drop out of +the generated scaffolding. + +For example, we pass the raw nextjs `request` object to your handler implementations, +allowing you full control where its needed. +```typescript +export const GET = _GET(async ({params}, respond, request) => { + console.log(request.nextUrl.buildId) + // ...your implementation here +}) +``` + +Use sparingly - the goal is to reduce the need for escape hatches over time. diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts index 9b476fbfc..0d0e76026 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts @@ -98,7 +98,7 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { } innerFunction?.addParameter({name: "respond"}) - innerFunction?.addParameter({name: "context"}) + innerFunction?.addParameter({name: "request"}) } // TODO: duplication - should be shared with router builder diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts index 6f5975bf1..2cac55d49 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs.generator.ts @@ -116,9 +116,7 @@ export async function generateTypescriptNextJS( ) ).flat() - const clientOutputPath = [generatedDirectory, "clients", "client.ts"].join( - path.sep, - ) + const clientOutputPath = [generatedDirectory, "client.ts"].join(path.sep) const clientImportBuilder = new ImportBuilder( {filename: clientOutputPath}, importAlias, From 732d26b04ccb1e185cd1b83dafc58aaf45f19cf0 Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 15:50:21 +0100 Subject: [PATCH 14/15] feat: onError handler --- .../app/todo-lists.yaml/attachments/route.ts | 32 ++- .../list/[listId]/items/route.ts | 32 ++- .../todo-lists.yaml/list/[listId]/route.ts | 48 +++- .../src/app/todo-lists.yaml/list/route.ts | 16 +- .../todo-lists.yaml/attachments/route.ts | 118 +++++---- .../todo-lists.yaml/{clients => }/client.ts | 2 +- .../list/[listId]/items/route.ts | 164 ++++++------ .../todo-lists.yaml/list/[listId]/route.ts | 245 ++++++++++-------- .../generated/todo-lists.yaml/list/route.ts | 63 +++-- .../src/core/file-system/node-fs-adaptor.ts | 11 +- .../typescript-nextjs-app-router-builder.ts | 42 ++- .../typescript-nextjs-router-builder.ts | 6 +- 12 files changed, 469 insertions(+), 310 deletions(-) rename integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/{clients => }/client.ts (99%) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts index 1941329f7..c471fabe4 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/attachments/route.ts @@ -1,10 +1,26 @@ import {_GET, _POST} from "../../../generated/todo-lists.yaml/attachments/route" -export const GET = _GET(async (respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) -export const POST = _POST(async ({body}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) +export const GET = _GET( + async (respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) +export const POST = _POST( + async ({body}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts index 002c82e4b..a6485c654 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/items/route.ts @@ -3,11 +3,27 @@ import { _POST, } from "../../../../../generated/todo-lists.yaml/list/[listId]/items/route" -export const GET = _GET(async ({params}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) -export const POST = _POST(async ({params, body}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) +export const GET = _GET( + async ({params}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) +export const POST = _POST( + async ({params, body}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts index e305d93de..cd5430ff6 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/[listId]/route.ts @@ -4,15 +4,39 @@ import { _PUT, } from "../../../../generated/todo-lists.yaml/list/[listId]/route" -export const GET = _GET(async ({params}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) -export const PUT = _PUT(async ({params, body}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) -export const DELETE = _DELETE(async ({params}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) +export const GET = _GET( + async ({params}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) +export const PUT = _PUT( + async ({params, body}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) +export const DELETE = _DELETE( + async ({params}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) diff --git a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts index 1eeb847f0..9765805fa 100644 --- a/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/app/todo-lists.yaml/list/route.ts @@ -1,6 +1,14 @@ import {_GET} from "../../../generated/todo-lists.yaml/list/route" -export const GET = _GET(async ({query}, respond, request) => { - // TODO: implementation - return respond.withStatus(501).body({message: "not implemented"} as any) -}) +export const GET = _GET( + async ({query}, respond, request) => { + // TODO: implementation + return respond.withStatus(501).body({message: "not implemented"} as any) + }, + async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), { + status: 501, + }) + }, +) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts index d0636f656..de9aea186 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -38,69 +38,83 @@ export type UploadAttachment = ( ) => Promise> export const _GET = - (implementation: ListAttachments) => + ( + implementation: ListAttachments, + onError: (err: unknown) => Promise, + ) => async (request: NextRequest): Promise => { - const input = { - params: undefined, - // TODO: this swallows repeated parameters - query: undefined, - body: undefined, - headers: undefined, - } + try { + const input = { + params: undefined, + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } - const responder = { - with200() { - return new OpenAPIRuntimeResponse(200) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - } + const responder = { + with200() { + return new OpenAPIRuntimeResponse(200) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } - const {status, body} = await implementation(responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) + const {status, body} = await implementation(responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) + } } const uploadAttachmentBodySchema = z.object({file: z.unknown().optional()}) export const _POST = - (implementation: UploadAttachment) => + ( + implementation: UploadAttachment, + onError: (err: unknown) => Promise, + ) => async (request: NextRequest): Promise => { - const input = { - params: undefined, - // TODO: this swallows repeated parameters - query: undefined, - body: parseRequestInput( - uploadAttachmentBodySchema, - await request.json(), - RequestInputType.RequestBody, - ), - headers: undefined, - } + try { + const input = { + params: undefined, + // TODO: this swallows repeated parameters + query: undefined, + body: parseRequestInput( + uploadAttachmentBodySchema, + await request.json(), + RequestInputType.RequestBody, + ), + headers: undefined, + } - const responder = { - with202() { - return new OpenAPIRuntimeResponse(202) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - } + const responder = { + with202() { + return new OpenAPIRuntimeResponse(202) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) + } } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/client.ts similarity index 99% rename from integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts rename to integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/client.ts index 925e0e07e..053a38dcc 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/clients/client.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/client.ts @@ -8,7 +8,7 @@ import { t_Statuses, t_TodoList, t_UnknownObject, -} from "../models" +} from "./models" import { AbstractFetchClient, AbstractFetchClientConfig, diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts index 1e15413f1..0450b1a88 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts @@ -60,52 +60,59 @@ export type CreateTodoListItem = ( const getTodoListItemsParamSchema = z.object({listId: z.string()}) export const _GET = - (implementation: GetTodoListItems) => + ( + implementation: GetTodoListItems, + onError: (err: unknown) => Promise, + ) => async ( request: NextRequest, {params}: {params: Promise}, ): Promise => { - const input = { - params: parseRequestInput( - getTodoListItemsParamSchema, - await params, - RequestInputType.RouteParam, - ), - // TODO: this swallows repeated parameters - query: undefined, - body: undefined, - headers: undefined, + try { + const input = { + params: parseRequestInput( + getTodoListItemsParamSchema, + await params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new OpenAPIRuntimeResponse<{ + completedAt?: string + content: string + createdAt: string + id: string + }>(200) + }, + withStatusCode5xx(status: StatusCode5xx) { + return new OpenAPIRuntimeResponse<{ + code: string + message: string + }>(status) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } + + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) } - - const responder = { - with200() { - return new OpenAPIRuntimeResponse<{ - completedAt?: string - content: string - createdAt: string - id: string - }>(200) - }, - withStatusCode5xx(status: StatusCode5xx) { - return new OpenAPIRuntimeResponse<{ - code: string - message: string - }>(status) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - } - - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) - - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) } const createTodoListItemParamSchema = z.object({listId: z.string()}) @@ -117,43 +124,50 @@ const createTodoListItemBodySchema = z.object({ }) export const _POST = - (implementation: CreateTodoListItem) => + ( + implementation: CreateTodoListItem, + onError: (err: unknown) => Promise, + ) => async ( request: NextRequest, {params}: {params: Promise}, ): Promise => { - const input = { - params: parseRequestInput( - createTodoListItemParamSchema, - await params, - RequestInputType.RouteParam, - ), - // TODO: this swallows repeated parameters - query: undefined, - body: parseRequestInput( - createTodoListItemBodySchema, - await request.json(), - RequestInputType.RequestBody, - ), - headers: undefined, + try { + const input = { + params: parseRequestInput( + createTodoListItemParamSchema, + await params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: parseRequestInput( + createTodoListItemBodySchema, + await request.json(), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with204() { + return new OpenAPIRuntimeResponse(204) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } + + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) } - - const responder = { - with204() { - return new OpenAPIRuntimeResponse(204) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - } - - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) - - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts index 5fed57e82..eea0caa3c 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts @@ -71,47 +71,54 @@ export type DeleteTodoListById = ( const getTodoListByIdParamSchema = z.object({listId: z.string()}) export const _GET = - (implementation: GetTodoListById) => + ( + implementation: GetTodoListById, + onError: (err: unknown) => Promise, + ) => async ( request: NextRequest, {params}: {params: Promise}, ): Promise => { - const input = { - params: parseRequestInput( - getTodoListByIdParamSchema, - await params, - RequestInputType.RouteParam, - ), - // TODO: this swallows repeated parameters - query: undefined, - body: undefined, - headers: undefined, + try { + const input = { + params: parseRequestInput( + getTodoListByIdParamSchema, + await params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with200() { + return new OpenAPIRuntimeResponse(200) + }, + withStatusCode4xx(status: StatusCode4xx) { + return new OpenAPIRuntimeResponse(status) + }, + withDefault(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } + + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) } - - const responder = { - with200() { - return new OpenAPIRuntimeResponse(200) - }, - withStatusCode4xx(status: StatusCode4xx) { - return new OpenAPIRuntimeResponse(status) - }, - withDefault(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - } - - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) - - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) } const updateTodoListByIdParamSchema = z.object({listId: z.string()}) @@ -119,95 +126,109 @@ const updateTodoListByIdParamSchema = z.object({listId: z.string()}) const updateTodoListByIdBodySchema = s_CreateUpdateTodoList export const _PUT = - (implementation: UpdateTodoListById) => + ( + implementation: UpdateTodoListById, + onError: (err: unknown) => Promise, + ) => async ( request: NextRequest, {params}: {params: Promise}, ): Promise => { - const input = { - params: parseRequestInput( - updateTodoListByIdParamSchema, - await params, - RequestInputType.RouteParam, - ), - // TODO: this swallows repeated parameters - query: undefined, - body: parseRequestInput( - updateTodoListByIdBodySchema, - await request.json(), - RequestInputType.RequestBody, - ), - headers: undefined, - } - - const responder = { - with200() { - return new OpenAPIRuntimeResponse(200) - }, - withStatusCode4xx(status: StatusCode4xx) { - return new OpenAPIRuntimeResponse(status) - }, - withDefault(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, + try { + const input = { + params: parseRequestInput( + updateTodoListByIdParamSchema, + await params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: parseRequestInput( + updateTodoListByIdBodySchema, + await request.json(), + RequestInputType.RequestBody, + ), + headers: undefined, + } + + const responder = { + with200() { + return new OpenAPIRuntimeResponse(200) + }, + withStatusCode4xx(status: StatusCode4xx) { + return new OpenAPIRuntimeResponse(status) + }, + withDefault(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } + + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) } - - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) - - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) } const deleteTodoListByIdParamSchema = z.object({listId: z.string()}) export const _DELETE = - (implementation: DeleteTodoListById) => + ( + implementation: DeleteTodoListById, + onError: (err: unknown) => Promise, + ) => async ( request: NextRequest, {params}: {params: Promise}, ): Promise => { - const input = { - params: parseRequestInput( - deleteTodoListByIdParamSchema, - await params, - RequestInputType.RouteParam, - ), - // TODO: this swallows repeated parameters - query: undefined, - body: undefined, - headers: undefined, - } - - const responder = { - with204() { - return new OpenAPIRuntimeResponse(204) - }, - withStatusCode4xx(status: StatusCode4xx) { - return new OpenAPIRuntimeResponse(status) - }, - withDefault(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, + try { + const input = { + params: parseRequestInput( + deleteTodoListByIdParamSchema, + await params, + RequestInputType.RouteParam, + ), + // TODO: this swallows repeated parameters + query: undefined, + body: undefined, + headers: undefined, + } + + const responder = { + with204() { + return new OpenAPIRuntimeResponse(204) + }, + withStatusCode4xx(status: StatusCode4xx) { + return new OpenAPIRuntimeResponse(status) + }, + withDefault(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } + + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) } - - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) - - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts index 40d6da0b7..6d8971848 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -46,36 +46,43 @@ const getTodoListsQuerySchema = z.object({ }) export const _GET = - (implementation: GetTodoLists) => + ( + implementation: GetTodoLists, + onError: (err: unknown) => Promise, + ) => async (request: NextRequest): Promise => { - const input = { - params: undefined, - // TODO: this swallows repeated parameters - query: parseRequestInput( - getTodoListsQuerySchema, - Object.fromEntries(request.nextUrl.searchParams.entries()), - RequestInputType.QueryString, - ), - body: undefined, - headers: undefined, - } + try { + const input = { + params: undefined, + // TODO: this swallows repeated parameters + query: parseRequestInput( + getTodoListsQuerySchema, + Object.fromEntries(request.nextUrl.searchParams.entries()), + RequestInputType.QueryString, + ), + body: undefined, + headers: undefined, + } - const responder = { - with200() { - return new OpenAPIRuntimeResponse(200) - }, - withStatus(status: StatusCode) { - return new OpenAPIRuntimeResponse(status) - }, - } + const responder = { + with200() { + return new OpenAPIRuntimeResponse(200) + }, + withStatus(status: StatusCode) { + return new OpenAPIRuntimeResponse(status) + }, + } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) - .catch((err) => { - throw OpenAPIRuntimeError.HandlerError(err) - }) + const {status, body} = await implementation(input, responder, request) + .then((it) => it.unpack()) + .catch((err) => { + throw OpenAPIRuntimeError.HandlerError(err) + }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + } catch (err) { + return await onError(err) + } } diff --git a/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts b/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts index f9fcfd473..832e0c4a8 100644 --- a/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts +++ b/packages/openapi-code-generator/src/core/file-system/node-fs-adaptor.ts @@ -13,8 +13,15 @@ export class NodeFsAdaptor implements IFsAdaptor { } async exists(path: string) { - const stat = await fs.stat(path) - return stat.isFile() + try { + const stat = await fs.stat(path) + return stat.isFile() + } catch (err) { + if (err instanceof Error && Reflect.get(err, "code") === "ENOENT") { + return false + } + throw err + } } existsSync(path: string) { diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts index 0d0e76026..05abce8c7 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts @@ -1,8 +1,8 @@ import { type SourceFile, StructureKind, - SyntaxKind, VariableDeclarationKind, + ts, } from "ts-morph" import type {Input} from "../../../core/input" import type {IROperation} from "../../../core/openapi-types-normalized" @@ -18,6 +18,7 @@ import type {SchemaBuilder} from "../../common/schema-builders/schema-builder" import type {TypeBuilder} from "../../common/type-builder" import type {ServerSymbols} from "../abstract-router-builder" import {ServerOperationBuilder} from "../server-operation-builder" +import SyntaxKind = ts.SyntaxKind export class TypescriptNextjsAppRouterBuilder implements ICompilable { constructor( @@ -65,6 +66,9 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { initializer: `${wrappingMethod}(async (input, respond, context) => { // TODO: implementation return respond.withStatus(501).body({message: "not implemented"} as any) + }, async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), {status: 501}) })`, }, ], @@ -73,19 +77,22 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { // Replace the params based on what inputs we have // biome-ignore lint/style/noNonNullAssertion: const declarations = variableDeclaration.getDeclarations()[0]! + const callExpression = declarations.getInitializerIfKindOrThrow( + SyntaxKind.CallExpression, + ) + // biome-ignore lint/style/noNonNullAssertion: - const innerFunction = declarations - .getInitializerIfKindOrThrow(SyntaxKind.CallExpression) + const implementationFunction = callExpression .getArguments()[0]! .asKind(SyntaxKind.ArrowFunction)! // biome-ignore lint/complexity/noForEach: - innerFunction?.getParameters().forEach((parameter) => { + implementationFunction?.getParameters().forEach((parameter) => { parameter.remove() }) if (params.hasParams) { - innerFunction?.addParameter({ + implementationFunction?.addParameter({ name: `{${[ params.path.schema ? "params" : undefined, params.query.schema ? "query" : undefined, @@ -97,8 +104,29 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable { }) } - innerFunction?.addParameter({name: "respond"}) - innerFunction?.addParameter({name: "request"}) + implementationFunction?.addParameter({name: "respond"}) + implementationFunction?.addParameter({name: "request"}) + + const onErrorFunction = callExpression.getArguments()[1] + + if (!onErrorFunction) { + callExpression.addArgument(`async (err) => { + // TODO: implementation + return new Response(JSON.stringify({message: "not implemented"}), {status: 501}) + }`) + } else if (onErrorFunction.getKind() === SyntaxKind.ArrowFunction) { + for (const param of onErrorFunction + .asKind(SyntaxKind.ArrowFunction) + ?.getParameters() ?? []) { + param.remove() + } + + onErrorFunction + ?.asKind(SyntaxKind.ArrowFunction) + ?.addParameter({name: "err"}) + } else if (onErrorFunction.getKind() === SyntaxKind.FunctionExpression) { + // todo + } } // TODO: duplication - should be shared with router builder diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts index f05e79b2e..2e12d82ab 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts @@ -117,7 +117,8 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { buildExport({ name: `_${builder.method.toUpperCase()}`, kind: "const", - value: `(implementation: ${symbols.implTypeName}) => async (${["request: NextRequest", params.path.schema ? "{params}: {params: Promise}" : undefined].filter(isDefined).join(",")}): Promise => { + value: `(implementation: ${symbols.implTypeName}, onError: (err: unknown) => Promise) => async (${["request: NextRequest", params.path.schema ? "{params}: {params: Promise}" : undefined].filter(isDefined).join(",")}): Promise => { +try { const input = { params: ${ params.path.schema @@ -152,6 +153,9 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder { .catch(err => { throw OpenAPIRuntimeError.HandlerError(err) }) return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) + } catch (err) { + return await onError(err) + } }`, }), ) From 1bb21169929daf3b788f9de14397631440f241bd Mon Sep 17 00:00:00 2001 From: Michael Nahkies Date: Sat, 5 Jul 2025 16:15:03 +0100 Subject: [PATCH 15/15] feat: support returning a raw Response --- .../todo-lists.yaml/attachments/route.ts | 34 +++++++++---- .../list/[listId]/items/route.ts | 34 +++++++++---- .../todo-lists.yaml/list/[listId]/route.ts | 51 +++++++++++++------ .../generated/todo-lists.yaml/list/route.ts | 17 +++++-- .../typescript-nextjs-router-builder.ts | 16 +++--- .../typescript-nextjs-runtime/src/server.ts | 2 - 6 files changed, 106 insertions(+), 48 deletions(-) diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts index de9aea186..f1bcb2668 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/attachments/route.ts @@ -61,15 +61,22 @@ export const _GET = }, } - const {status, body} = await implementation(responder, request) - .then((it) => it.unpack()) + const res = await implementation(responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } @@ -105,15 +112,22 @@ export const _POST = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts index 0450b1a88..bf394fb06 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/items/route.ts @@ -101,15 +101,22 @@ export const _GET = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } @@ -158,15 +165,22 @@ export const _POST = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts index eea0caa3c..52b3d1ff9 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/[listId]/route.ts @@ -107,15 +107,22 @@ export const _GET = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } @@ -166,15 +173,22 @@ export const _PUT = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } @@ -219,15 +233,22 @@ export const _DELETE = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } diff --git a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts index 6d8971848..10a1938e2 100644 --- a/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts +++ b/integration-tests/typescript-nextjs/src/generated/todo-lists.yaml/list/route.ts @@ -73,15 +73,22 @@ export const _GET = }, } - const {status, body} = await implementation(input, responder, request) - .then((it) => it.unpack()) + const res = await implementation(input, responder, request) + .then((it) => { + if (it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined + ? Response.json(body, {status}) + : new Response(undefined, {status}) + }) .catch((err) => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined - ? Response.json(body, {status}) - : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } diff --git a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts index 2e12d82ab..a6ab0fa61 100644 --- a/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts +++ b/packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts @@ -145,14 +145,18 @@ try { const responder = ${responder.implementation} - const { - status, - body, - } = await implementation(${[params.hasParams ? "input" : undefined, "responder", "request"].filter(isDefined).join(",")}) - .then(it => it.unpack()) + const res = await implementation(${[params.hasParams ? "input" : undefined, "responder", "request"].filter(isDefined).join(",")}) + .then(it => { + if(it instanceof Response) { + return it + } + const {status, body} = it.unpack() + + return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) + }) .catch(err => { throw OpenAPIRuntimeError.HandlerError(err) }) - return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status}) + return res } catch (err) { return await onError(err) } diff --git a/packages/typescript-nextjs-runtime/src/server.ts b/packages/typescript-nextjs-runtime/src/server.ts index 3a9c01860..6767e06c8 100644 --- a/packages/typescript-nextjs-runtime/src/server.ts +++ b/packages/typescript-nextjs-runtime/src/server.ts @@ -29,8 +29,6 @@ export type Response = { body: Type } -export const SkipResponse = Symbol("skip response processing") - export class OpenAPIRuntimeResponse { private _body?: Type