From db35e5e6fc2b4652706bf7a42a9441fbcf5e8444 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 20:19:49 +0100 Subject: [PATCH 01/12] vue -server-functions --- e2e/vue-start/server-functions/.gitignore | 20 + .../server-functions/.prettierignore | 4 + e2e/vue-start/server-functions/package.json | 42 ++ .../server-functions/playwright.config.ts | 35 + .../server-functions/postcss.config.mjs | 5 + .../server-functions/public/favicon.ico | Bin 0 -> 15406 bytes .../server-functions/public/favicon.png | Bin 0 -> 1507 bytes .../src/components/DefaultCatchBoundary.tsx | 53 ++ .../src/components/NotFound.tsx | 25 + .../server-functions/src/routeTree.gen.ts | 621 ++++++++++++++++++ e2e/vue-start/server-functions/src/router.tsx | 32 + .../server-functions/src/routes/__root.tsx | 47 ++ .../src/routes/abort-signal.tsx | 85 +++ .../src/routes/consistent.tsx | 121 ++++ .../src/routes/cookies/index.tsx | 24 + .../src/routes/cookies/set.tsx | 66 ++ .../src/routes/dead-code-preserve.tsx | 61 ++ .../server-functions/src/routes/env-only.tsx | 77 +++ .../factory/-functions/createBarServerFn.ts | 22 + .../routes/factory/-functions/createFakeFn.ts | 5 + .../factory/-functions/createFooServerFn.ts | 24 + .../routes/factory/-functions/functions.ts | 93 +++ .../src/routes/factory/index.tsx | 202 ++++++ .../src/routes/formdata-redirect/index.tsx | 74 +++ .../routes/formdata-redirect/target.$name.tsx | 15 + .../server-functions/src/routes/headers.tsx | 80 +++ .../server-functions/src/routes/index.tsx | 78 +++ .../src/routes/isomorphic-fns.tsx | 79 +++ .../middleware/client-middleware-router.tsx | 79 +++ .../src/routes/middleware/index.tsx | 37 ++ .../routes/middleware/request-middleware.tsx | 83 +++ .../src/routes/middleware/send-serverFn.tsx | 78 +++ .../server-functions/src/routes/multipart.tsx | 107 +++ .../src/routes/primitives/index.tsx | 132 ++++ .../src/routes/raw-response.tsx | 47 ++ .../src/routes/redirect-test-ssr/index.tsx | 32 + .../src/routes/redirect-test-ssr/target.tsx | 14 + .../src/routes/redirect-test/index.tsx | 32 + .../src/routes/redirect-test/target.tsx | 14 + .../src/routes/return-null.tsx | 68 ++ .../src/routes/serialize-form-data.tsx | 85 +++ .../server-functions/src/routes/status.tsx | 30 + .../src/routes/submit-post-formdata.tsx | 60 ++ .../server-functions/src/styles/app.css | 30 + .../server-functions/src/vite-env.d.ts | 4 + .../tests/server-functions.spec.ts | 497 ++++++++++++++ e2e/vue-start/server-functions/tsconfig.json | 23 + e2e/vue-start/server-functions/vite.config.ts | 32 + pnpm-lock.yaml | 76 +++ 49 files changed, 3550 insertions(+) create mode 100644 e2e/vue-start/server-functions/.gitignore create mode 100644 e2e/vue-start/server-functions/.prettierignore create mode 100644 e2e/vue-start/server-functions/package.json create mode 100644 e2e/vue-start/server-functions/playwright.config.ts create mode 100644 e2e/vue-start/server-functions/postcss.config.mjs create mode 100644 e2e/vue-start/server-functions/public/favicon.ico create mode 100644 e2e/vue-start/server-functions/public/favicon.png create mode 100644 e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx create mode 100644 e2e/vue-start/server-functions/src/components/NotFound.tsx create mode 100644 e2e/vue-start/server-functions/src/routeTree.gen.ts create mode 100644 e2e/vue-start/server-functions/src/router.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/__root.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/abort-signal.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/consistent.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/cookies/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/cookies/set.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/env-only.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts create mode 100644 e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts create mode 100644 e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts create mode 100644 e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts create mode 100644 e2e/vue-start/server-functions/src/routes/factory/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/headers.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/middleware/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/multipart.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/primitives/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/raw-response.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/return-null.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/status.tsx create mode 100644 e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx create mode 100644 e2e/vue-start/server-functions/src/styles/app.css create mode 100644 e2e/vue-start/server-functions/src/vite-env.d.ts create mode 100644 e2e/vue-start/server-functions/tests/server-functions.spec.ts create mode 100644 e2e/vue-start/server-functions/tsconfig.json create mode 100644 e2e/vue-start/server-functions/vite.config.ts diff --git a/e2e/vue-start/server-functions/.gitignore b/e2e/vue-start/server-functions/.gitignore new file mode 100644 index 00000000000..a79d5cf1299 --- /dev/null +++ b/e2e/vue-start/server-functions/.gitignore @@ -0,0 +1,20 @@ +node_modules +package-lock.json +yarn.lock + +.DS_Store +.cache +.env +.vercel +.output + +/build/ +/api/ +/server/build +/public/build +# Sentry Config File +.env.sentry-build-plugin +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/vue-start/server-functions/.prettierignore b/e2e/vue-start/server-functions/.prettierignore new file mode 100644 index 00000000000..2be5eaa6ece --- /dev/null +++ b/e2e/vue-start/server-functions/.prettierignore @@ -0,0 +1,4 @@ +**/build +**/public +pnpm-lock.yaml +routeTree.gen.ts \ No newline at end of file diff --git a/e2e/vue-start/server-functions/package.json b/e2e/vue-start/server-functions/package.json new file mode 100644 index 00000000000..33384c9e564 --- /dev/null +++ b/e2e/vue-start/server-functions/package.json @@ -0,0 +1,42 @@ +{ + "name": "tanstack-vue-start-e2e-server-functions", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "dev": "vite dev --port 3000", + "dev:e2e": "vite dev", + "build": "vite build && tsc --noEmit", + "preview": "vite preview", + "start": "pnpx srvx --prod -s ../client dist/server/server.js", + "test:e2e": "rm -rf port*.txt; playwright test --project=chromium" + }, + "dependencies": { + "@tanstack/vue-query": "^5.90.9", + "@tanstack/vue-router": "workspace:^", + "@tanstack/vue-router-devtools": "workspace:^", + "@tanstack/vue-router-ssr-query": "workspace:^", + "@tanstack/vue-start": "workspace:^", + "js-cookie": "^3.0.5", + "redaxios": "^0.5.1", + "tailwind-merge": "^2.6.0", + "vite": "^7.1.7", + "vue": "^3.5.25", + "zod": "^3.24.2" + }, + "devDependencies": { + "@playwright/test": "^1.50.1", + "@tailwindcss/postcss": "^4.1.15", + "@tanstack/router-e2e-utils": "workspace:^", + "@types/js-cookie": "^3.0.6", + "@types/node": "^22.10.2", + "combinate": "^1.1.11", + "postcss": "^8.5.1", + "srvx": "^0.8.6", + "tailwindcss": "^4.1.17", + "typescript": "^5.7.2", + "@vitejs/plugin-vue": "^6.0.3", + "@vitejs/plugin-vue-jsx": "^5.1.2", + "vite-tsconfig-paths": "^5.1.4" + } +} diff --git a/e2e/vue-start/server-functions/playwright.config.ts b/e2e/vue-start/server-functions/playwright.config.ts new file mode 100644 index 00000000000..cb1da03b942 --- /dev/null +++ b/e2e/vue-start/server-functions/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' +import { getTestServerPort } from '@tanstack/router-e2e-utils' +import packageJson from './package.json' with { type: 'json' } + +export const PORT = await getTestServerPort(packageJson.name) +const baseURL = `http://localhost:${PORT}` + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + workers: 1, + + reporter: [['line']], + + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL, + }, + + webServer: { + command: `pnpm build && VITE_SERVER_PORT=${PORT} PORT=${PORT} pnpm start`, + url: baseURL, + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}) diff --git a/e2e/vue-start/server-functions/postcss.config.mjs b/e2e/vue-start/server-functions/postcss.config.mjs new file mode 100644 index 00000000000..a7f73a2d1d7 --- /dev/null +++ b/e2e/vue-start/server-functions/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +} diff --git a/e2e/vue-start/server-functions/public/favicon.ico b/e2e/vue-start/server-functions/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1a1751676f7e22811b1070572093996c93c87617 GIT binary patch literal 15406 zcmeHOd0bW1+Fl2T)asg*b?Ynb=5X`&-CNczGfPshnW^PmhMJl=CJLyC8iErjB7!1= z%=0WD0t$kNf(oc84uDKf;D7_*z&VHWeDAyW*>FJYTG{>QyXW_NS$nU&*84nb?X}k4 z`*{~as6-p_+;f7`H^iK_LVPHMc;gNE{H-oR_)y-v@9MAj79y*w5N}Z#szNp7d`ceg z=Qg#k@cO}B`2AEQLYAsU^lG)(?NlVveB4D=RNqHBi7@LZyk>X`-?=&wyaXc324dGH zh`sI*2ZA9E$3YxV(}}Zro+2xvqoE%&Gttr5;%^xu$Xs8~f$F(IWCTHE$5Opih%-kZ z&Yy-jl?h|pAsJjp@v(NPk*BSN3PZOKf=D3D{ee_(C&aN7h|`CuUIE0#a)`n_3=NqA zF3WYeew3H!8|bXk`EOAn+)ag*2_NI>WPgaGyY-kWm?m!BVg-cSkCwHgSkV7%d$ihpd+fwB2n%=`AHbdAe!S+2u%Eu2wg?hGhq zwxvNjHX7#*6PqjedU_4aH|QF#E9E%lx@LY*lYwoauNnjVw_<^p8Xd=Mg_*Aoi+ts4 zN|_d^dU>2qy*yrrap8M0DKs1JWdDHC?g#MKIbq=Z1<_TMHt0PiYimy5!@5g#XqNzpXtEec~usxTf6PbkDqAu50ezz_=_Pt%P-o2*Owy3VuMqO8Gt*$AvExLMsqx-eXE{~qS zii2O7@;dVd*=JmqJ_o=9-? z5_?=tM2bh}-;Jj@@SNIPxKH*Gp409N?^zK33m}3lAi}I5BCR2Iu7!x-2$8sj?%{Tb zeO|oI+!u!;eZ-O7wCeuGpU13DgzG3gzSl^&em@Z|t%ISGQ;FG zj@PMUDH>6b=_qn@JN+sazO#E#dkcj3kD&D)BG3?bjRCGJMCuM|uYwyx>th1p?uE$D zfGEg@IF|=elwTk+f_ps)XL|`ZeLtxMtK|OPZ5E)4U?wID2aEW|}8@+;m!x z4}?NwMa#H(jJuz3vmnmqO6#*IE0mrS9a6lnvF~5vU^-3onloN?ZJ2p)h+t}S*m9cF zt7Y5-#@$Bk^@K3QJ+ccTZx6(YbizHJ87#T90#y9nQl8gMTKBV9#Q+w0snR`&i zEn?iWgj+(m7a=OE_h_WL2e&@vCYu7I&AMA^LD*hRZ zF%=H6KEh|KjS3Ey)b1rJY+j*)FJY&Kt5BLFu;*YO^a+cCD#b&-2S@0gC7jN5 zoa`9APtcglO@fNXf1lk4uqXQ+sV@6qU+j~8GX`TZCga=Nmvqib9eBU!$n&^xTu4@y z*B<$qy|FibGCVv(VQG6G7OQ}1b~hn5_|W{PIi5y#D1zpC4B8*sjif>1xtnzOXnY;!ZKQWI_M!J9)z=>z`sL%sYx4Cxb1z&s^P>DmSkEnHn75-wx^C)0 z?~fxK(e5i}EcDdEYzJWKp?hTANBLCpCG246%z_BN6`SpU1ApE39r}4WN!Mq((fIq) z0dGtTZnb=CK7KKeu$RV=MeCs0lIRAE@=KJ?#|EV1gA?=c*ObZlF{}cUw$R)jz5xTR z(i+Pv^?p+tqtjU@>8@KR>OiSvOA~I>yW-~<7nX=GgTnC6;UDnsk(u}?z#b#k(K`FN zEvC8^HkP;8RgH0>$yk}F*5@@)%GTub7mly5%h2Vm%V>aN)@e29vF97~**68fJ?5d$ z{wa7PVH{oy9g7baN1)A+6|hOUkLmGQcrS7(-aha>dPYrctgrZayi}Lxn4|UDl%s_s zy*tyfWZfgjqfh!|={@(z)28TudLf2JyEN8i zACf=4FU9Bd@CGS=Y#`0ky^UC2uBWvo+X}R3G7b7it^niy581Oj2BM4KU_9?XgvQ=< zbTl6?^-quFiBi9G4<8TvW7iDo8~V~>N<@QntzUo+&Zo4Pn%)4LT)7Nmdz7HFSE=Sc z85CQ4vKTLV4WkRj()U8A?fvo8)_zdU8-^F?JK}|af1zveFg)iw2p@;9#OU4b7#>fH ziGdHtld``NJ83NBYp{;KQQS*3*hJqMPGpS9*!&C#u2lO3RjFZUcIVFEPuo62yDc9; zFcUBk*R}1h`$Pkm^R(`CTD99djA2QPbX~tE@OPQ2(l*#%z@L~-t4h3Qt9(w;`4u>C< z^vb?_=34gM(|D9cU)hKG2iDQ}iEXt^`mHl?I#Y(Eo9FQ6kq7kdM%aAcWxGb$t-gOU zKL1YK&FPze=fJi6+Zo8eeL!z~tehJj^Yy0u?5l?`JLV$h?Z1HIw+^5~W&^!16E@pE zToWnsceRZ4=)Wa*_Vy~i5nE7vJqEwdb|RxV2?xs)rFze2Q~NUr`vCQM#xJ+KC7UZ( zJUU&f^mV*)WrybSl^u9o+nkt*31P)JUK)&{Cn_`|o5osh>-W1QW^3oyFFE$EzTn_< zv%>EFtqMEbs<0>HwB@mUUS8;g>T>)0)fYDToW11PY>u_&|8etBV&D0G$qJMEC01Vb z=PmQp=a*hrmn_v$%67fJ#4?YsaTzZAxPJe?mt&oTBw8_z?1|_ku) zoLL*GBuyrszS%8BcG!C&J)KnX|G>{)hWhd9%iUkiJv1Vr0!CCz14$y>;SLhK0yK^pc=Y zswdVK&nd>jb80eaS8{**P=71DIrhMsoy41B5UkrVZ;nN)qOAH>NFSsP>Rgf)xeQ#w&}yhLOjUk!YK0%q%b#eR zETVV4#j;izu~LrRNcx=}^*63x>)y#!CJ#HHoO>HxC?nG7X z+(||lv5YlK3weGjdTA{6cf7v8lN8>h*QWW(F*MeS4SDA#lXjabYpAU4ojI)Nw{nb4 z;#~r9se;Fjq%DfQ_`DT<(;e72bKQT^JZPNl*SI#ZA<#uAm2%b+9;S4 zb7PK=YRBR!;-#gtRmscdt8`ZLRbaE6tAgpAr_gufFtlahb&{|Z z9?XfkF~>*o4{;S1n^&sT8%T?^Un*<8&Z|`L-bC?BpAHxkIb6Ta(D+Gm)@#4i-^`o! z?wlk!hRT}v$xPy%E$hIAq{k|}%N5?#->e5$U8V6v<#-*XwvS2q5rKYBOPGw!db7lZ zI59Wo*c$%`578|#MARu-u3@@6SRg(?Alh4CqQ?L{yK@y(2{itB4Dpy@?i~Ali1%?> zE9dp3C2#KY@*+v&SCO9m?4b}$4EkEaU@XQo)*V-lin-MQ64L-J@Y)2co$Q= zp-k5OS%c^Gh1VNi^Qq5`a&}=*?rONC{gZsRl`t5KF&UdVD14Y3b7Zc}S!qLgzIg9= zs<@aGq(ay>(&z0}@LW&&HjSG|cNNkiRXDLv;Os$x@;rfxV=C;~I|LKm_v3|FdY1BB zke;s`FQWUw>m}b0=E&opjo14;T8H>Of#(Que<3Xc6Mb{BCv_+)j;kc!jKNrp$=J++ zxiBZ@#vGX|b7uZFHZVGw+0(M zCf;6l0CQK|gT>FJuahtK$-Wtbu^5xF6>VPTVnlj<2QXLW%-omR-R`o^>2&-yk9hb6 zY)4q=TI`Hkiny3Xh>Bc}kdO`V^7Vn!_B7g0a0M2&v=5+#nbWx#O{nZS14b z(=CN;Ke}z%i~b?!FvzbIz2@z~NV8%rGNbtYCucEZz(p*!)HUvc3j2#uRT;jr< zn43RwWUkDaxi49R9_DtaG+$3Tx!xArX|dRz`qz&1bA$X}I#zv2YwBbgHDzF8 zv!n#`S3kgqgH!P1vOAbK?luO!UWOTc?!(qt1MAnd*z&0cOU;{bTl3Exm|76Th^%(M19n98H{~7FCc@oDG z_w7jH*okD@DOIdRo;l}J-cPP~vB32~Q+a(kF^t|TCip{)cEc#E6X5dSt(}TLun@DnuQ!(a zVQV#{{{Pw)-M;f~%x}%d6V9tKBklQd?OWdycx~rb`1_$57~~bySnnIhQknmVP55-_ z{>J>r_4|9uEs4@WHhPYeQ@&N4u13E%tl3_%W$_ve@NvQ0o>nl8 zxh7qE$72=VJvtKu&Y4Luj=r9&VHKxEfAcuvzaCx2IbnWKbu&MWd(V_TXiqS;ir3Yw zO4b#wqP=O9lIhbuI{chek57U&6VIs>ubYp>3D@a)IuHNInt`{{Owc!HHeU0afVr_n z={F9HMb;@Axk zgID5X%UIa%Q`5f3I~0e^#`{4l@uL6dcr$qdUiKXQ5JpSP)_6QrrWsFdlKnxAUE^NC zL((2WY44!@Aq|FxyHcEXCO*iYkDiI&qLcHdQf!dphduU8#G8o|(A&uz&y2K2yP+#E zc5^0XC+6UvAuG^pw+a4vd@hDuw4!@83qzuudH>-r81GqZetkW~Ib?1WTckdo5k~P` zDNioP+?{f@BOEF2$hNtKjgJdMucS$MGl_VnPLg7+F9v;%S0hJCG1%8*N8_2F$H3@c zi}1{s))>6q8{GrH#XA(2?sw`Z^ga3`r3>(vo!?;b{?iZnXS~*M6(0R*AH(83a+&3{ zkFuXD@y~AJ$=qE|J?OFZl(v!#EzLYL53dD|p?)5Zm&1okdp$W$$Z_L8Q4ICZl-J&h zz9|RIMcdIc(bfGc^r3O}_e0b1I>i=y?)?_MQ@+E%s5RJhyyhYQE%Er=jAEOc@?_52by4IP61rcJ%Gc>t8gl~ z^$?CB?tpC#n7m7i?ZjvC5iP!Q12p%*ovSFvckj9B8jBW7`tP_oEuHnPS;H$~15-kyCp*x285Y7E9&S z%$d3KH(20hycbxhxfn<>>DJ7p^fKNFo{OiP`{5~X4H&%38iChpAHoQ{rpBy;S`1HZ zKqzt8cu9kS6xVOhyg9}lP8LcQqEDmXOQajW-?c<+qC4$B=|pp(ozp+5-#?MYPZ!$%z?HqgZ`2{e=1R zFF~WRh}YDs$)MOSI(E98kA5)=@T$*9yzKo2Ui0}1qf*wvySf6O?Xkq$)W6&wo*Pf| zJ@7P^>;k@O$a}ZIz7)TldR?u@zaq4FJB0R<&^?HJP*2YadKceKT$Mcq zysvdmBk) zOHW169-vY5TpKH`IqhjqPd?y?IY&IO^2|>7SD&MDcVu7WNAVe1Q;YZqwREipZdYrm zeKnX_R!^EL@#K98F%KE-r$#d6KTNEi4{YG>45J zC$4l*T|6`EUSaK_d*_hV!dm7j=dsrg!DR1p^zs=6la!yK6p(IGx+}l zCGW_c!^pgOP%gvQTb5PM4O1#-Ra$}ev|mm7e+B-Zg(j<}V^bpa*zpT)LopJcI&~-0 z^wh2N+EcgEAX_@6iZ#zW*;t12l`@5mt74@F25SArvEpg|26sjR#p{) zoYEM?6zoO*#YlQj$iy>;)fB&>H8PXdnJk*CPw2<%()p@@mntj0Eh?|L*HvD2$L}?p z$Sl0M<~Ba|yNuMck;p6$!)v)Ub>b+k?}uoOB+Ms7znPnxSGIJ!alz4-_VHZ2dBH(_ z^TI|*R^dP?oBmunHau7IIdwqs*=;B~w+%NdHmTVc`}8RJgZ2+JYk@Q`+TJeT_+Cxf z8q2z})$w(ut18LxtE|kXlIyY$_C<58+51cj$Uo$i=lAW3WnCT=uk7)l#BxM^3GHGp sUYw*kZ&9czwx}V4-fB3n{`}%3F2iNH4%cNLe+aq%I{j}CJVp=vAC(LAUjP6A literal 0 HcmV?d00001 diff --git a/e2e/vue-start/server-functions/public/favicon.png b/e2e/vue-start/server-functions/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e77bc06091ade4496525a09d8900675afcf03f0 GIT binary patch literal 1507 zcmV<91swW`P)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91AfN*P1ONa40RR91AOHXW0IY^$^8f$^O-V#SR9Fd>S3Qp$MHK9ro!#4$ zEP@L_hX|b@f=!*_42h6mKu7{)4)_U*>1>0bCkUj;Z1X!7 zHe(Ew^Oi(|bW3J~xu+)XbtFF?4>!7TH$>(D_atUQVEj(8fGvYu2NF33#JZX>)(Vj8 zIi@z>Glt?6t~;Lf(|C8F>;WF^8F<^s7Scr!sZc01uB?HMHoL5+FZ>B(g+r-)?Sn)#3Zal#?G@GAwO5U27MpGOlC2+_saA)rl zP-<@-n~;PQOlm|Hi<+W;NdR;5+=zADzM&?!+CPD36=cGwHy6!D^vPEHG?rO`K>G|M z3FposX{yT132wuw1OR3Um_5JoKB#6?!QgBupIT;?YIr;WcpmuCE>S75mZid+ens#E zGPuYjiG0UNNVWu=f!Id^?9)34)eIpu-`j_~W0iAQzK(}XYc_!;87Tk~?4tq|h=2(! zuq0HCiNK)@+ocCKR3q1REdUju>HdYxd>JX@%oOibg+J~D+}rhz54D!NfC{h-OYk{M zkzmFtdrL@nL0bm8nF@pob1CeLC>12ef#in-Bzv2!wi)Iuwq24)`AH}|0QNQ^f$KHv z?5PBPo1*#GAuAk+Poe`?UJ>mP`@~d4a(103j0lwUx@_+$#B&VC%7r>#2$HIiD`KO8L|s3Yp%M}BT0;NJDzZtPnx=4%enhU zhW*pNN0t`^4%5MKAR+}=^Q?QeqQ`>bbK zf+-ji$Uz8V0?LpX@kh`k%DL)GCA2=@SJNKg56Wh>>pr=7{1PmHqG|~=AdLV3002ov JPDHLkV1ivgp)>#h literal 0 HcmV?d00001 diff --git a/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx b/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx new file mode 100644 index 00000000000..d6b3c732885 --- /dev/null +++ b/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx @@ -0,0 +1,53 @@ +import { + ErrorComponent, + Link, + rootRouteId, + useMatch, + useRouter, +} from '@tanstack/vue-router' +import type { ErrorComponentProps } from '@tanstack/vue-router' + +export function DefaultCatchBoundary({ error }: ErrorComponentProps) { + const router = useRouter() + const isRoot = useMatch({ + strict: false, + select: (state) => state.id === rootRouteId, + }) + + console.error(error) + + return ( +
+ +
+ + {isRoot() ? ( + + Home + + ) : ( + { + e.preventDefault() + window.history.back() + }} + > + Go Back + + )} +
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/components/NotFound.tsx b/e2e/vue-start/server-functions/src/components/NotFound.tsx new file mode 100644 index 00000000000..944e35c12c6 --- /dev/null +++ b/e2e/vue-start/server-functions/src/components/NotFound.tsx @@ -0,0 +1,25 @@ +import { Link } from '@tanstack/vue-router' + +export function NotFound({ children }: { children?: any }) { + return ( +
+
+ {children ||

The page you are looking for does not exist.

} +
+

+ + + Start Over + +

+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routeTree.gen.ts b/e2e/vue-start/server-functions/src/routeTree.gen.ts new file mode 100644 index 00000000000..91633bf3c79 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routeTree.gen.ts @@ -0,0 +1,621 @@ +/* eslint-disable */ + +// @ts-nocheck + +// noinspection JSUnusedGlobalSymbols + +// This file was automatically generated by TanStack Router. +// You should NOT make any changes in this file as it will be overwritten. +// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. + +import { Route as rootRouteImport } from './routes/__root' +import { Route as SubmitPostFormdataRouteImport } from './routes/submit-post-formdata' +import { Route as StatusRouteImport } from './routes/status' +import { Route as SerializeFormDataRouteImport } from './routes/serialize-form-data' +import { Route as ReturnNullRouteImport } from './routes/return-null' +import { Route as RawResponseRouteImport } from './routes/raw-response' +import { Route as MultipartRouteImport } from './routes/multipart' +import { Route as IsomorphicFnsRouteImport } from './routes/isomorphic-fns' +import { Route as HeadersRouteImport } from './routes/headers' +import { Route as EnvOnlyRouteImport } from './routes/env-only' +import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserve' +import { Route as ConsistentRouteImport } from './routes/consistent' +import { Route as AbortSignalRouteImport } from './routes/abort-signal' +import { Route as IndexRouteImport } from './routes/index' +import { Route as RedirectTestIndexRouteImport } from './routes/redirect-test/index' +import { Route as RedirectTestSsrIndexRouteImport } from './routes/redirect-test-ssr/index' +import { Route as PrimitivesIndexRouteImport } from './routes/primitives/index' +import { Route as MiddlewareIndexRouteImport } from './routes/middleware/index' +import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index' +import { Route as FactoryIndexRouteImport } from './routes/factory/index' +import { Route as CookiesIndexRouteImport } from './routes/cookies/index' +import { Route as RedirectTestTargetRouteImport } from './routes/redirect-test/target' +import { Route as RedirectTestSsrTargetRouteImport } from './routes/redirect-test-ssr/target' +import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn' +import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware' +import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' +import { Route as CookiesSetRouteImport } from './routes/cookies/set' +import { Route as FormdataRedirectTargetNameRouteImport } from './routes/formdata-redirect/target.$name' + +const SubmitPostFormdataRoute = SubmitPostFormdataRouteImport.update({ + id: '/submit-post-formdata', + path: '/submit-post-formdata', + getParentRoute: () => rootRouteImport, +} as any) +const StatusRoute = StatusRouteImport.update({ + id: '/status', + path: '/status', + getParentRoute: () => rootRouteImport, +} as any) +const SerializeFormDataRoute = SerializeFormDataRouteImport.update({ + id: '/serialize-form-data', + path: '/serialize-form-data', + getParentRoute: () => rootRouteImport, +} as any) +const ReturnNullRoute = ReturnNullRouteImport.update({ + id: '/return-null', + path: '/return-null', + getParentRoute: () => rootRouteImport, +} as any) +const RawResponseRoute = RawResponseRouteImport.update({ + id: '/raw-response', + path: '/raw-response', + getParentRoute: () => rootRouteImport, +} as any) +const MultipartRoute = MultipartRouteImport.update({ + id: '/multipart', + path: '/multipart', + getParentRoute: () => rootRouteImport, +} as any) +const IsomorphicFnsRoute = IsomorphicFnsRouteImport.update({ + id: '/isomorphic-fns', + path: '/isomorphic-fns', + getParentRoute: () => rootRouteImport, +} as any) +const HeadersRoute = HeadersRouteImport.update({ + id: '/headers', + path: '/headers', + getParentRoute: () => rootRouteImport, +} as any) +const EnvOnlyRoute = EnvOnlyRouteImport.update({ + id: '/env-only', + path: '/env-only', + getParentRoute: () => rootRouteImport, +} as any) +const DeadCodePreserveRoute = DeadCodePreserveRouteImport.update({ + id: '/dead-code-preserve', + path: '/dead-code-preserve', + getParentRoute: () => rootRouteImport, +} as any) +const ConsistentRoute = ConsistentRouteImport.update({ + id: '/consistent', + path: '/consistent', + getParentRoute: () => rootRouteImport, +} as any) +const AbortSignalRoute = AbortSignalRouteImport.update({ + id: '/abort-signal', + path: '/abort-signal', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const RedirectTestIndexRoute = RedirectTestIndexRouteImport.update({ + id: '/redirect-test/', + path: '/redirect-test/', + getParentRoute: () => rootRouteImport, +} as any) +const RedirectTestSsrIndexRoute = RedirectTestSsrIndexRouteImport.update({ + id: '/redirect-test-ssr/', + path: '/redirect-test-ssr/', + getParentRoute: () => rootRouteImport, +} as any) +const PrimitivesIndexRoute = PrimitivesIndexRouteImport.update({ + id: '/primitives/', + path: '/primitives/', + getParentRoute: () => rootRouteImport, +} as any) +const MiddlewareIndexRoute = MiddlewareIndexRouteImport.update({ + id: '/middleware/', + path: '/middleware/', + getParentRoute: () => rootRouteImport, +} as any) +const FormdataRedirectIndexRoute = FormdataRedirectIndexRouteImport.update({ + id: '/formdata-redirect/', + path: '/formdata-redirect/', + getParentRoute: () => rootRouteImport, +} as any) +const FactoryIndexRoute = FactoryIndexRouteImport.update({ + id: '/factory/', + path: '/factory/', + getParentRoute: () => rootRouteImport, +} as any) +const CookiesIndexRoute = CookiesIndexRouteImport.update({ + id: '/cookies/', + path: '/cookies/', + getParentRoute: () => rootRouteImport, +} as any) +const RedirectTestTargetRoute = RedirectTestTargetRouteImport.update({ + id: '/redirect-test/target', + path: '/redirect-test/target', + getParentRoute: () => rootRouteImport, +} as any) +const RedirectTestSsrTargetRoute = RedirectTestSsrTargetRouteImport.update({ + id: '/redirect-test-ssr/target', + path: '/redirect-test-ssr/target', + getParentRoute: () => rootRouteImport, +} as any) +const MiddlewareSendServerFnRoute = MiddlewareSendServerFnRouteImport.update({ + id: '/middleware/send-serverFn', + path: '/middleware/send-serverFn', + getParentRoute: () => rootRouteImport, +} as any) +const MiddlewareRequestMiddlewareRoute = + MiddlewareRequestMiddlewareRouteImport.update({ + id: '/middleware/request-middleware', + path: '/middleware/request-middleware', + getParentRoute: () => rootRouteImport, + } as any) +const MiddlewareClientMiddlewareRouterRoute = + MiddlewareClientMiddlewareRouterRouteImport.update({ + id: '/middleware/client-middleware-router', + path: '/middleware/client-middleware-router', + getParentRoute: () => rootRouteImport, + } as any) +const CookiesSetRoute = CookiesSetRouteImport.update({ + id: '/cookies/set', + path: '/cookies/set', + getParentRoute: () => rootRouteImport, +} as any) +const FormdataRedirectTargetNameRoute = + FormdataRedirectTargetNameRouteImport.update({ + id: '/formdata-redirect/target/$name', + path: '/formdata-redirect/target/$name', + getParentRoute: () => rootRouteImport, + } as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/abort-signal': typeof AbortSignalRoute + '/consistent': typeof ConsistentRoute + '/dead-code-preserve': typeof DeadCodePreserveRoute + '/env-only': typeof EnvOnlyRoute + '/headers': typeof HeadersRoute + '/isomorphic-fns': typeof IsomorphicFnsRoute + '/multipart': typeof MultipartRoute + '/raw-response': typeof RawResponseRoute + '/return-null': typeof ReturnNullRoute + '/serialize-form-data': typeof SerializeFormDataRoute + '/status': typeof StatusRoute + '/submit-post-formdata': typeof SubmitPostFormdataRoute + '/cookies/set': typeof CookiesSetRoute + '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute + '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute + '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute + '/redirect-test/target': typeof RedirectTestTargetRoute + '/cookies': typeof CookiesIndexRoute + '/factory': typeof FactoryIndexRoute + '/formdata-redirect': typeof FormdataRedirectIndexRoute + '/middleware': typeof MiddlewareIndexRoute + '/primitives': typeof PrimitivesIndexRoute + '/redirect-test-ssr': typeof RedirectTestSsrIndexRoute + '/redirect-test': typeof RedirectTestIndexRoute + '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/abort-signal': typeof AbortSignalRoute + '/consistent': typeof ConsistentRoute + '/dead-code-preserve': typeof DeadCodePreserveRoute + '/env-only': typeof EnvOnlyRoute + '/headers': typeof HeadersRoute + '/isomorphic-fns': typeof IsomorphicFnsRoute + '/multipart': typeof MultipartRoute + '/raw-response': typeof RawResponseRoute + '/return-null': typeof ReturnNullRoute + '/serialize-form-data': typeof SerializeFormDataRoute + '/status': typeof StatusRoute + '/submit-post-formdata': typeof SubmitPostFormdataRoute + '/cookies/set': typeof CookiesSetRoute + '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute + '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute + '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute + '/redirect-test/target': typeof RedirectTestTargetRoute + '/cookies': typeof CookiesIndexRoute + '/factory': typeof FactoryIndexRoute + '/formdata-redirect': typeof FormdataRedirectIndexRoute + '/middleware': typeof MiddlewareIndexRoute + '/primitives': typeof PrimitivesIndexRoute + '/redirect-test-ssr': typeof RedirectTestSsrIndexRoute + '/redirect-test': typeof RedirectTestIndexRoute + '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/abort-signal': typeof AbortSignalRoute + '/consistent': typeof ConsistentRoute + '/dead-code-preserve': typeof DeadCodePreserveRoute + '/env-only': typeof EnvOnlyRoute + '/headers': typeof HeadersRoute + '/isomorphic-fns': typeof IsomorphicFnsRoute + '/multipart': typeof MultipartRoute + '/raw-response': typeof RawResponseRoute + '/return-null': typeof ReturnNullRoute + '/serialize-form-data': typeof SerializeFormDataRoute + '/status': typeof StatusRoute + '/submit-post-formdata': typeof SubmitPostFormdataRoute + '/cookies/set': typeof CookiesSetRoute + '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute + '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute + '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute + '/redirect-test-ssr/target': typeof RedirectTestSsrTargetRoute + '/redirect-test/target': typeof RedirectTestTargetRoute + '/cookies/': typeof CookiesIndexRoute + '/factory/': typeof FactoryIndexRoute + '/formdata-redirect/': typeof FormdataRedirectIndexRoute + '/middleware/': typeof MiddlewareIndexRoute + '/primitives/': typeof PrimitivesIndexRoute + '/redirect-test-ssr/': typeof RedirectTestSsrIndexRoute + '/redirect-test/': typeof RedirectTestIndexRoute + '/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: + | '/' + | '/abort-signal' + | '/consistent' + | '/dead-code-preserve' + | '/env-only' + | '/headers' + | '/isomorphic-fns' + | '/multipart' + | '/raw-response' + | '/return-null' + | '/serialize-form-data' + | '/status' + | '/submit-post-formdata' + | '/cookies/set' + | '/middleware/client-middleware-router' + | '/middleware/request-middleware' + | '/middleware/send-serverFn' + | '/redirect-test-ssr/target' + | '/redirect-test/target' + | '/cookies' + | '/factory' + | '/formdata-redirect' + | '/middleware' + | '/primitives' + | '/redirect-test-ssr' + | '/redirect-test' + | '/formdata-redirect/target/$name' + fileRoutesByTo: FileRoutesByTo + to: + | '/' + | '/abort-signal' + | '/consistent' + | '/dead-code-preserve' + | '/env-only' + | '/headers' + | '/isomorphic-fns' + | '/multipart' + | '/raw-response' + | '/return-null' + | '/serialize-form-data' + | '/status' + | '/submit-post-formdata' + | '/cookies/set' + | '/middleware/client-middleware-router' + | '/middleware/request-middleware' + | '/middleware/send-serverFn' + | '/redirect-test-ssr/target' + | '/redirect-test/target' + | '/cookies' + | '/factory' + | '/formdata-redirect' + | '/middleware' + | '/primitives' + | '/redirect-test-ssr' + | '/redirect-test' + | '/formdata-redirect/target/$name' + id: + | '__root__' + | '/' + | '/abort-signal' + | '/consistent' + | '/dead-code-preserve' + | '/env-only' + | '/headers' + | '/isomorphic-fns' + | '/multipart' + | '/raw-response' + | '/return-null' + | '/serialize-form-data' + | '/status' + | '/submit-post-formdata' + | '/cookies/set' + | '/middleware/client-middleware-router' + | '/middleware/request-middleware' + | '/middleware/send-serverFn' + | '/redirect-test-ssr/target' + | '/redirect-test/target' + | '/cookies/' + | '/factory/' + | '/formdata-redirect/' + | '/middleware/' + | '/primitives/' + | '/redirect-test-ssr/' + | '/redirect-test/' + | '/formdata-redirect/target/$name' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + AbortSignalRoute: typeof AbortSignalRoute + ConsistentRoute: typeof ConsistentRoute + DeadCodePreserveRoute: typeof DeadCodePreserveRoute + EnvOnlyRoute: typeof EnvOnlyRoute + HeadersRoute: typeof HeadersRoute + IsomorphicFnsRoute: typeof IsomorphicFnsRoute + MultipartRoute: typeof MultipartRoute + RawResponseRoute: typeof RawResponseRoute + ReturnNullRoute: typeof ReturnNullRoute + SerializeFormDataRoute: typeof SerializeFormDataRoute + StatusRoute: typeof StatusRoute + SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute + CookiesSetRoute: typeof CookiesSetRoute + MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute + MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute + MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute + RedirectTestSsrTargetRoute: typeof RedirectTestSsrTargetRoute + RedirectTestTargetRoute: typeof RedirectTestTargetRoute + CookiesIndexRoute: typeof CookiesIndexRoute + FactoryIndexRoute: typeof FactoryIndexRoute + FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute + MiddlewareIndexRoute: typeof MiddlewareIndexRoute + PrimitivesIndexRoute: typeof PrimitivesIndexRoute + RedirectTestSsrIndexRoute: typeof RedirectTestSsrIndexRoute + RedirectTestIndexRoute: typeof RedirectTestIndexRoute + FormdataRedirectTargetNameRoute: typeof FormdataRedirectTargetNameRoute +} + +declare module '@tanstack/vue-router' { + interface FileRoutesByPath { + '/submit-post-formdata': { + id: '/submit-post-formdata' + path: '/submit-post-formdata' + fullPath: '/submit-post-formdata' + preLoaderRoute: typeof SubmitPostFormdataRouteImport + parentRoute: typeof rootRouteImport + } + '/status': { + id: '/status' + path: '/status' + fullPath: '/status' + preLoaderRoute: typeof StatusRouteImport + parentRoute: typeof rootRouteImport + } + '/serialize-form-data': { + id: '/serialize-form-data' + path: '/serialize-form-data' + fullPath: '/serialize-form-data' + preLoaderRoute: typeof SerializeFormDataRouteImport + parentRoute: typeof rootRouteImport + } + '/return-null': { + id: '/return-null' + path: '/return-null' + fullPath: '/return-null' + preLoaderRoute: typeof ReturnNullRouteImport + parentRoute: typeof rootRouteImport + } + '/raw-response': { + id: '/raw-response' + path: '/raw-response' + fullPath: '/raw-response' + preLoaderRoute: typeof RawResponseRouteImport + parentRoute: typeof rootRouteImport + } + '/multipart': { + id: '/multipart' + path: '/multipart' + fullPath: '/multipart' + preLoaderRoute: typeof MultipartRouteImport + parentRoute: typeof rootRouteImport + } + '/isomorphic-fns': { + id: '/isomorphic-fns' + path: '/isomorphic-fns' + fullPath: '/isomorphic-fns' + preLoaderRoute: typeof IsomorphicFnsRouteImport + parentRoute: typeof rootRouteImport + } + '/headers': { + id: '/headers' + path: '/headers' + fullPath: '/headers' + preLoaderRoute: typeof HeadersRouteImport + parentRoute: typeof rootRouteImport + } + '/env-only': { + id: '/env-only' + path: '/env-only' + fullPath: '/env-only' + preLoaderRoute: typeof EnvOnlyRouteImport + parentRoute: typeof rootRouteImport + } + '/dead-code-preserve': { + id: '/dead-code-preserve' + path: '/dead-code-preserve' + fullPath: '/dead-code-preserve' + preLoaderRoute: typeof DeadCodePreserveRouteImport + parentRoute: typeof rootRouteImport + } + '/consistent': { + id: '/consistent' + path: '/consistent' + fullPath: '/consistent' + preLoaderRoute: typeof ConsistentRouteImport + parentRoute: typeof rootRouteImport + } + '/abort-signal': { + id: '/abort-signal' + path: '/abort-signal' + fullPath: '/abort-signal' + preLoaderRoute: typeof AbortSignalRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/redirect-test/': { + id: '/redirect-test/' + path: '/redirect-test' + fullPath: '/redirect-test' + preLoaderRoute: typeof RedirectTestIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/redirect-test-ssr/': { + id: '/redirect-test-ssr/' + path: '/redirect-test-ssr' + fullPath: '/redirect-test-ssr' + preLoaderRoute: typeof RedirectTestSsrIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/primitives/': { + id: '/primitives/' + path: '/primitives' + fullPath: '/primitives' + preLoaderRoute: typeof PrimitivesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/middleware/': { + id: '/middleware/' + path: '/middleware' + fullPath: '/middleware' + preLoaderRoute: typeof MiddlewareIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/formdata-redirect/': { + id: '/formdata-redirect/' + path: '/formdata-redirect' + fullPath: '/formdata-redirect' + preLoaderRoute: typeof FormdataRedirectIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/factory/': { + id: '/factory/' + path: '/factory' + fullPath: '/factory' + preLoaderRoute: typeof FactoryIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/cookies/': { + id: '/cookies/' + path: '/cookies' + fullPath: '/cookies' + preLoaderRoute: typeof CookiesIndexRouteImport + parentRoute: typeof rootRouteImport + } + '/redirect-test/target': { + id: '/redirect-test/target' + path: '/redirect-test/target' + fullPath: '/redirect-test/target' + preLoaderRoute: typeof RedirectTestTargetRouteImport + parentRoute: typeof rootRouteImport + } + '/redirect-test-ssr/target': { + id: '/redirect-test-ssr/target' + path: '/redirect-test-ssr/target' + fullPath: '/redirect-test-ssr/target' + preLoaderRoute: typeof RedirectTestSsrTargetRouteImport + parentRoute: typeof rootRouteImport + } + '/middleware/send-serverFn': { + id: '/middleware/send-serverFn' + path: '/middleware/send-serverFn' + fullPath: '/middleware/send-serverFn' + preLoaderRoute: typeof MiddlewareSendServerFnRouteImport + parentRoute: typeof rootRouteImport + } + '/middleware/request-middleware': { + id: '/middleware/request-middleware' + path: '/middleware/request-middleware' + fullPath: '/middleware/request-middleware' + preLoaderRoute: typeof MiddlewareRequestMiddlewareRouteImport + parentRoute: typeof rootRouteImport + } + '/middleware/client-middleware-router': { + id: '/middleware/client-middleware-router' + path: '/middleware/client-middleware-router' + fullPath: '/middleware/client-middleware-router' + preLoaderRoute: typeof MiddlewareClientMiddlewareRouterRouteImport + parentRoute: typeof rootRouteImport + } + '/cookies/set': { + id: '/cookies/set' + path: '/cookies/set' + fullPath: '/cookies/set' + preLoaderRoute: typeof CookiesSetRouteImport + parentRoute: typeof rootRouteImport + } + '/formdata-redirect/target/$name': { + id: '/formdata-redirect/target/$name' + path: '/formdata-redirect/target/$name' + fullPath: '/formdata-redirect/target/$name' + preLoaderRoute: typeof FormdataRedirectTargetNameRouteImport + parentRoute: typeof rootRouteImport + } + } +} + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + AbortSignalRoute: AbortSignalRoute, + ConsistentRoute: ConsistentRoute, + DeadCodePreserveRoute: DeadCodePreserveRoute, + EnvOnlyRoute: EnvOnlyRoute, + HeadersRoute: HeadersRoute, + IsomorphicFnsRoute: IsomorphicFnsRoute, + MultipartRoute: MultipartRoute, + RawResponseRoute: RawResponseRoute, + ReturnNullRoute: ReturnNullRoute, + SerializeFormDataRoute: SerializeFormDataRoute, + StatusRoute: StatusRoute, + SubmitPostFormdataRoute: SubmitPostFormdataRoute, + CookiesSetRoute: CookiesSetRoute, + MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, + MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute, + MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute, + RedirectTestSsrTargetRoute: RedirectTestSsrTargetRoute, + RedirectTestTargetRoute: RedirectTestTargetRoute, + CookiesIndexRoute: CookiesIndexRoute, + FactoryIndexRoute: FactoryIndexRoute, + FormdataRedirectIndexRoute: FormdataRedirectIndexRoute, + MiddlewareIndexRoute: MiddlewareIndexRoute, + PrimitivesIndexRoute: PrimitivesIndexRoute, + RedirectTestSsrIndexRoute: RedirectTestSsrIndexRoute, + RedirectTestIndexRoute: RedirectTestIndexRoute, + FormdataRedirectTargetNameRoute: FormdataRedirectTargetNameRoute, +} +export const routeTree = rootRouteImport + ._addFileChildren(rootRouteChildren) + ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/vue-start' +declare module '@tanstack/vue-start' { + interface Register { + ssr: true + router: Awaited> + } +} diff --git a/e2e/vue-start/server-functions/src/router.tsx b/e2e/vue-start/server-functions/src/router.tsx new file mode 100644 index 00000000000..11122f6b8a9 --- /dev/null +++ b/e2e/vue-start/server-functions/src/router.tsx @@ -0,0 +1,32 @@ +import { createRouter } from '@tanstack/vue-router' +import { setupRouterSsrQueryIntegration } from '@tanstack/vue-router-ssr-query' +import { QueryClient } from '@tanstack/vue-query' +import { routeTree } from './routeTree.gen' +import { DefaultCatchBoundary } from './components/DefaultCatchBoundary' +import { NotFound } from './components/NotFound' + +export function getRouter() { + const queryClient = new QueryClient() + const router = createRouter({ + routeTree, + defaultPreload: 'intent', + defaultErrorComponent: DefaultCatchBoundary, + defaultNotFoundComponent: () => , + scrollRestoration: true, + context: { + foo: { + bar: 'baz', + }, + }, + }) + + setupRouterSsrQueryIntegration({ router, queryClient }) + + return router +} + +declare module '@tanstack/vue-router' { + interface Register { + router: ReturnType + } +} diff --git a/e2e/vue-start/server-functions/src/routes/__root.tsx b/e2e/vue-start/server-functions/src/routes/__root.tsx new file mode 100644 index 00000000000..329896a7c02 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/__root.tsx @@ -0,0 +1,47 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/vue-router' + +import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools' +import { HydrationScript } from 'solid-js/web' +import { NotFound } from '~/components/NotFound' +import appCss from '~/styles/app.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + ], + links: [{ rel: 'stylesheet', href: appCss }], + }), + errorComponent: (props) => { + return

{props.error.stack}

+ }, + notFoundComponent: () => , + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + + + + + + + + + ) +} diff --git a/e2e/vue-start/server-functions/src/routes/abort-signal.tsx b/e2e/vue-start/server-functions/src/routes/abort-signal.tsx new file mode 100644 index 00000000000..4b3e6c969df --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/abort-signal.tsx @@ -0,0 +1,85 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn } from '@tanstack/vue-start' +import * as Solid from 'solid-js' + +export const Route = createFileRoute('/abort-signal')({ + component: RouteComponent, +}) + +const abortableServerFn = createServerFn().handler( + async ({ context, signal }) => { + console.log('server function started', { context, signal }) + return new Promise((resolve, reject) => { + if (signal.aborted) { + return reject(new Error('Aborted before start')) + } + const timerId = setTimeout(() => { + console.log('server function finished') + resolve('server function result') + }, 1000) + const onAbort = () => { + clearTimeout(timerId) + console.log('server function aborted') + reject(new Error('Aborted')) + } + signal.addEventListener('abort', onAbort, { once: true }) + }) + }, +) + +function RouteComponent() { + const [errorMessage, setErrorMessage] = Solid.createSignal< + string | undefined + >(undefined) + const [result, setResult] = Solid.createSignal(undefined) + + const reset = () => { + setErrorMessage(undefined) + setResult(undefined) + } + return ( +
+ +
+ +
+ result:

{result() ?? '$undefined'}

+
+
+ message:{' '} +

{errorMessage() ?? '$undefined'}

+
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/consistent.tsx b/e2e/vue-start/server-functions/src/routes/consistent.tsx new file mode 100644 index 00000000000..738c2c63242 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/consistent.tsx @@ -0,0 +1,121 @@ +import { createFileRoute } from '@tanstack/vue-router' +import * as Solid from 'solid-js' +import { createServerFn } from '@tanstack/vue-start' + +/** + * This checks whether the returned payloads from a + * server function are the same, regardless of whether the server function is + * called directly from the client or from within the server function. + * @link https://github.com/TanStack/router/issues/1866 + * @link https://github.com/TanStack/router/issues/2481 + */ + +export const Route = createFileRoute('/consistent')({ + component: ConsistentServerFnCalls, + loader: async () => { + const data = await cons_serverGetFn1({ data: { username: 'TEST' } }) + console.log('cons_serverGetFn1', data) + return { data } + }, +}) + +const cons_getFn1 = createServerFn() + .inputValidator((d: { username: string }) => d) + .handler(({ data }) => { + return { payload: data } + }) + +const cons_serverGetFn1 = createServerFn() + .inputValidator((d: { username: string }) => d) + .handler(async ({ data }) => { + return cons_getFn1({ data }) + }) + +const cons_postFn1 = createServerFn({ method: 'POST' }) + .inputValidator((d: { username: string }) => d) + .handler(({ data }) => { + return { payload: data } + }) + +const cons_serverPostFn1 = createServerFn({ method: 'POST' }) + .inputValidator((d: { username: string }) => d) + .handler(({ data }) => { + return cons_postFn1({ data }) + }) + +function ConsistentServerFnCalls() { + const [getServerResult, setGetServerResult] = Solid.createSignal({}) + const [getDirectResult, setGetDirectResult] = Solid.createSignal({}) + + const [postServerResult, setPostServerResult] = Solid.createSignal({}) + const [postDirectResult, setPostDirectResult] = Solid.createSignal({}) + + return ( +
+

Consistent Server Fn GET Calls

+

+ This component checks whether the returned payloads from server function + are the same, regardless of whether the server function is called + directly from the client or from within the server function. +

+
+ It should return{' '} + +
+            {JSON.stringify({ payload: { username: 'TEST' } })}
+          
+
+
+

+ {`GET: cons_getFn1 called from server cons_serverGetFn1 returns`} +
+ + {JSON.stringify(getServerResult())} + +

+

+ {`GET: cons_getFn1 called directly returns`} +
+ + {JSON.stringify(getDirectResult())} + +

+

+ {`POST: cons_postFn1 called from cons_serverPostFn1 returns`} +
+ + {JSON.stringify(postServerResult())} + +

+

+ {`POST: cons_postFn1 called directly returns`} +
+ + {JSON.stringify(postDirectResult())} + +

+ +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/cookies/index.tsx b/e2e/vue-start/server-functions/src/routes/cookies/index.tsx new file mode 100644 index 00000000000..8a5f5a2a9c5 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/cookies/index.tsx @@ -0,0 +1,24 @@ +import { Link, createFileRoute } from '@tanstack/vue-router' +import { z } from 'zod' + +const cookieSchema = z + .object({ value: z.string() }) + .catch(() => ({ value: `CLIENT-${Date.now()}` })) +export const Route = createFileRoute('/cookies/')({ + validateSearch: cookieSchema, + component: RouteComponent, +}) + +function RouteComponent() { + const search = Route.useSearch() + return ( + + got to route that sets the cookies with {JSON.stringify(search())} + + ) +} diff --git a/e2e/vue-start/server-functions/src/routes/cookies/set.tsx b/e2e/vue-start/server-functions/src/routes/cookies/set.tsx new file mode 100644 index 00000000000..5491704a406 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/cookies/set.tsx @@ -0,0 +1,66 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn } from '@tanstack/vue-start' +import { setCookie } from '@tanstack/vue-start/server' +import { z } from 'zod' +import Cookies from 'js-cookie' +import * as Solid from 'solid-js' + +const cookieSchema = z.object({ value: z.string() }) + +export const Route = createFileRoute('/cookies/set')({ + validateSearch: cookieSchema, + loaderDeps: ({ search }) => search, + loader: async ({ deps }) => { + await setCookieServerFn1({ data: deps }) + await setCookieServerFn2({ data: deps }) + }, + component: RouteComponent, +}) + +export const setCookieServerFn1 = createServerFn() + .inputValidator(cookieSchema) + .handler(({ data }) => { + setCookie(`cookie-1-${data.value}`, data.value) + setCookie(`cookie-2-${data.value}`, data.value) + }) + +export const setCookieServerFn2 = createServerFn() + .inputValidator(cookieSchema) + .handler(({ data }) => { + setCookie(`cookie-3-${data.value}`, data.value) + setCookie(`cookie-4-${data.value}`, data.value) + }) + +function RouteComponent() { + const search = Route.useSearch() + const [cookiesFromDocument, setCookiesFromDocument] = Solid.createSignal< + Record | undefined + >(undefined) + Solid.createEffect(() => { + const tempCookies: Record = {} + for (let i = 1; i <= 4; i++) { + const key = `cookie-${i}-${search().value}` + tempCookies[key] = Cookies.get(key) + } + setCookiesFromDocument(tempCookies) + }, []) + return ( +
+

cookies result

+ + + + + + + {Object.entries(cookiesFromDocument() || {}).map(([key, value]) => ( + + + + + ))} + +
cookievalue
{key}{value}
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx new file mode 100644 index 00000000000..0d3ae3b9682 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx @@ -0,0 +1,61 @@ +import { createFileRoute } from '@tanstack/vue-router' +import * as fs from 'node:fs' +import { createServerFn } from '@tanstack/vue-start' +import { getRequestHeader } from '@tanstack/vue-start/server' +import { createSignal } from 'solid-js' +import {} from '@tanstack/vue-router' + +export const Route = createFileRoute('/dead-code-preserve')({ + component: RouteComponent, +}) + +// by using this we make sure DCE still works - this errors when imported on the client + +const filePath = 'count-effect.txt' + +async function readCount() { + return parseInt( + await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'), + ) +} + +async function updateCount() { + const count = await readCount() + await fs.promises.writeFile(filePath, `${count + 1}`) + return true +} + +const writeFileServerFn = createServerFn().handler(async () => { + // eslint-disable-next-line unused-imports/no-unused-vars + const test = await updateCount() + return getRequestHeader('X-Test') +}) + +const readFileServerFn = createServerFn().handler(async () => { + const data = await readCount() + return data +}) + +function RouteComponent() { + const [serverFnOutput, setServerFnOutput] = createSignal() + return ( +
+

Dead code test

+

+ This server function writes to a file as a side effect, then reads it. +

+ +

Server output

+
{serverFnOutput()}
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/env-only.tsx b/e2e/vue-start/server-functions/src/routes/env-only.tsx new file mode 100644 index 00000000000..a1bdf6ae229 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/env-only.tsx @@ -0,0 +1,77 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { + createClientOnlyFn, + createServerFn, + createServerOnlyFn, +} from '@tanstack/vue-start' +import { createSignal } from 'solid-js' + +const serverEcho = createServerOnlyFn((input: string) => 'server got: ' + input) +const clientEcho = createClientOnlyFn((input: string) => 'client got: ' + input) + +const testOnServer = createServerFn().handler(() => { + const serverOnServer = serverEcho('hello') + let clientOnServer: string + try { + clientOnServer = clientEcho('hello') + } catch (e) { + clientOnServer = + 'clientEcho threw an error: ' + + (e instanceof Error ? e.message : String(e)) + } + return { serverOnServer, clientOnServer } +}) + +export const Route = createFileRoute('/env-only')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [results, setResults] = createSignal>>() + + async function handleClick() { + const { serverOnServer, clientOnServer } = await testOnServer() + const clientOnClient = clientEcho('hello') + let serverOnClient: string + try { + serverOnClient = serverEcho('hello') + } catch (e) { + serverOnClient = + 'serverEcho threw an error: ' + + (e instanceof Error ? e.message : String(e)) + } + setResults({ + serverOnServer, + clientOnServer, + clientOnClient, + serverOnClient, + }) + } + + return ( +
+ + {!!results() && ( +
+

+ serverEcho +

+ When we called the function on the server: +
{results()?.serverOnServer}
+ When we called the function on the client: +
{results()?.serverOnClient}
+
+

+ clientEcho +

+ When we called the function on the server: +
{results()?.clientOnServer}
+ When we called the function on the client: +
{results()?.clientOnClient}
+
+ )} +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts new file mode 100644 index 00000000000..bff66859175 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/createBarServerFn.ts @@ -0,0 +1,22 @@ +import { createMiddleware } from '@tanstack/vue-start' +import { createFooServerFn } from './createFooServerFn' + +const barMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => { + console.log('Bar middleware triggered') + return next({ + context: { bar: 'bar' } as const, + }) + }, +) + +export const createBarServerFn = createFooServerFn().middleware([barMiddleware]) + +export const barFnInsideFactoryFile = createBarServerFn().handler( + ({ context }) => { + return { + name: 'barFnInsideFactoryFile', + context, + } + }, +) diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts new file mode 100644 index 00000000000..1c727338850 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFakeFn.ts @@ -0,0 +1,5 @@ +export function createFakeFn() { + return { + handler: (cb: () => Promise) => cb, + } +} diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts new file mode 100644 index 00000000000..d75084b7ffb --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/createFooServerFn.ts @@ -0,0 +1,24 @@ +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { getRequest } from '@tanstack/vue-start/server' + +const fooMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => { + const request = getRequest() + console.log('Foo middleware triggered') + return next({ + context: { foo: 'foo', method: request.method } as const, + }) + }, +) + +export const createFooServerFn = createServerFn().middleware([fooMiddleware]) + +export const fooFnInsideFactoryFile = createFooServerFn().handler( + async ({ context }) => { + console.log('fooFnInsideFactoryFile handler triggered', context.method) + return { + name: 'fooFnInsideFactoryFile', + context, + } + }, +) diff --git a/e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts b/e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts new file mode 100644 index 00000000000..190791a5996 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/factory/-functions/functions.ts @@ -0,0 +1,93 @@ +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { createBarServerFn } from './createBarServerFn' +import { createFooServerFn } from './createFooServerFn' +import { createFakeFn } from './createFakeFn' + +export const fooFn = createFooServerFn().handler(({ context }) => { + return { + name: 'fooFn', + context, + } +}) + +export const fooFnPOST = createFooServerFn({ method: 'POST' }).handler( + ({ context }) => { + return { + name: 'fooFnPOST', + context, + } + }, +) + +export const barFn = createBarServerFn().handler(({ context }) => { + return { + name: 'barFn', + context, + } +}) + +export const barFnPOST = createBarServerFn({ method: 'POST' }).handler( + ({ context }) => { + return { + name: 'barFnPOST', + context, + } + }, +) + +const localMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => { + console.log('local middleware triggered') + return next({ + context: { local: 'local' } as const, + }) + }, +) + +const localFnFactory = createBarServerFn.middleware([localMiddleware]) + +const anotherMiddleware = createMiddleware({ type: 'function' }).server( + ({ next }) => { + console.log('another middleware triggered') + return next({ + context: { another: 'another' } as const, + }) + }, +) + +export const localFn = localFnFactory() + .middleware([anotherMiddleware]) + .handler(({ context }) => { + return { + name: 'localFn', + context, + } + }) + +export const localFnPOST = localFnFactory({ method: 'POST' }) + .middleware([anotherMiddleware]) + .handler(({ context }) => { + return { + name: 'localFnPOST', + context, + } + }) + +export const fakeFn = createFakeFn().handler(async () => { + return { + name: 'fakeFn', + window, + } +}) + +export const composeFactory = createServerFn({ method: 'GET' }).middleware([ + createBarServerFn, +]) +export const composedFn = composeFactory() + .middleware([anotherMiddleware, localFnFactory]) + .handler(({ context }) => { + return { + name: 'composedFn', + context, + } + }) diff --git a/e2e/vue-start/server-functions/src/routes/factory/index.tsx b/e2e/vue-start/server-functions/src/routes/factory/index.tsx new file mode 100644 index 00000000000..83301e263e0 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/factory/index.tsx @@ -0,0 +1,202 @@ +import { createFileRoute, deepEqual } from '@tanstack/vue-router' + +import { createSignal, For } from 'solid-js' +import { createServerFn } from '@tanstack/vue-start' +import { fooFnInsideFactoryFile } from './-functions/createFooServerFn' +import { + barFn, + barFnPOST, + composedFn, + fakeFn, + fooFn, + fooFnPOST, + localFn, + localFnPOST, +} from './-functions/functions' + +export const Route = createFileRoute('/factory/')({ + ssr: false, + component: RouteComponent, +}) + +const fnInsideRoute = createServerFn({ method: 'GET' }).handler(() => { + return { + name: 'fnInsideRoute', + } +}) + +const functions = { + fnInsideRoute: { + fn: fnInsideRoute, + type: 'serverFn', + expected: { + name: 'fnInsideRoute', + }, + }, + fooFnInsideFactoryFile: { + fn: fooFnInsideFactoryFile, + type: 'serverFn', + + expected: { + name: 'fooFnInsideFactoryFile', + context: { foo: 'foo', method: 'GET' }, + }, + }, + fooFn: { + fn: fooFn, + type: 'serverFn', + + expected: { + name: 'fooFn', + context: { foo: 'foo', method: 'GET' }, + }, + }, + fooFnPOST: { + fn: fooFnPOST, + type: 'serverFn', + + expected: { + name: 'fooFnPOST', + context: { foo: 'foo', method: 'POST' }, + }, + }, + barFn: { + fn: barFn, + type: 'serverFn', + + expected: { + name: 'barFn', + context: { foo: 'foo', method: 'GET', bar: 'bar' }, + }, + }, + barFnPOST: { + fn: barFnPOST, + type: 'serverFn', + + expected: { + name: 'barFnPOST', + context: { foo: 'foo', method: 'POST', bar: 'bar' }, + }, + }, + localFn: { + fn: localFn, + type: 'serverFn', + + expected: { + name: 'localFn', + context: { + foo: 'foo', + method: 'GET', + bar: 'bar', + local: 'local', + another: 'another', + }, + }, + }, + localFnPOST: { + fn: localFnPOST, + type: 'serverFn', + + expected: { + name: 'localFnPOST', + context: { + foo: 'foo', + method: 'POST', + bar: 'bar', + local: 'local', + another: 'another', + }, + }, + }, + composedFn: { + fn: composedFn, + type: 'serverFn', + expected: { + name: 'composedFn', + context: { + foo: 'foo', + method: 'GET', + bar: 'bar', + another: 'another', + local: 'local', + }, + }, + }, + fakeFn: { + fn: fakeFn, + type: 'localFn', + expected: { + name: 'fakeFn', + window, + }, + }, +} satisfies Record + +interface TestCase { + fn: () => Promise + expected: any + type: 'serverFn' | 'localFn' +} +function Test(props: TestCase) { + const [result, setResult] = createSignal(null) + function comparison() { + if (result()) { + const isEqual = deepEqual(result(), props.expected) + return isEqual ? 'equal' : 'not equal' + } + return 'Loading...' + } + + return ( +
+

+
+ It should return{' '} + +
+            {props.type === 'serverFn'
+              ? JSON.stringify(props.expected)
+              : 'localFn'}
+          
+
+
+

+ fn returns: +
+ + {result() + ? props.type === 'serverFn' + ? JSON.stringify(result()) + : 'localFn' + : 'Loading...'} + {' '} + + {comparison()} + +

+ +
+ ) +} +function RouteComponent() { + return ( +
+

Server functions middleware E2E tests

+ + {([name, testCase]) => } + +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx new file mode 100644 index 00000000000..09eac5f1f64 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx @@ -0,0 +1,74 @@ +import { createFileRoute, redirect } from '@tanstack/vue-router' +import { createServerFn, useServerFn } from '@tanstack/vue-start' +import { z } from 'zod' + +export const Route = createFileRoute('/formdata-redirect/')({ + component: SubmitPostFormDataFn, + validateSearch: z.object({ + mode: z.union([z.literal('js'), z.literal('no-js')]).default('js'), + }), +}) + +const testValues = { + name: 'Sean', +} + +export const greetUser = createServerFn({ method: 'POST' }) + .inputValidator((data: FormData) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid! FormData is required') + } + const name = data.get('name') + + if (!name) { + throw new Error('Name is required') + } + + return { + name: name.toString(), + } + }) + .handler(({ data: { name } }) => { + throw redirect({ to: '/formdata-redirect/target/$name', params: { name } }) + }) + +function SubmitPostFormDataFn() { + const mode = Route.useSearch({ select: (search) => search.mode }) + const greetUserFn = useServerFn(greetUser) + return ( +
+

Submit POST FormData Fn Call

+
+ It should return redirect to /formdata-redirect/target/{testValues.name}{' '} + and greet the user with their name: + +
+            {testValues.name}
+          
+
+
+
{ + if (mode() === 'js') { + evt.preventDefault() + const data = new FormData(evt.currentTarget) + await greetUserFn({ data }) + } + }} + > + + +
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx new file mode 100644 index 00000000000..9d2595533d7 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/formdata-redirect/target/$name')({ + component: RouteComponent, +}) + +function RouteComponent() { + const params = Route.useParams() + return ( +
+ Hello{' '} + {params().name}! +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/headers.tsx b/e2e/vue-start/server-functions/src/routes/headers.tsx new file mode 100644 index 00000000000..805f5b1f08a --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/headers.tsx @@ -0,0 +1,80 @@ +import { createFileRoute } from '@tanstack/vue-router' +import * as Solid from 'solid-js' +import { createServerFn } from '@tanstack/vue-start' +import { + getRequestHeaders, + setResponseHeader, +} from '@tanstack/vue-start/server' +import type { RequestHeaderName } from '@tanstack/vue-start/server' + +export const Route = createFileRoute('/headers')({ + loader: async () => { + return { + testHeaders: await getTestHeaders(), + } + }, + component: () => { + const loaderData = Route.useLoaderData() + return + }, +}) + +export const getTestHeaders = createServerFn().handler(() => { + setResponseHeader('x-test-header', 'test-value') + const reqHeaders = Object.fromEntries(getRequestHeaders().entries()) + + return { + serverHeaders: reqHeaders, + headers: reqHeaders, + } +}) + +type TestHeadersResult = { + headers?: Partial> + serverHeaders?: Partial> +} + +function ResponseHeaders({ + initialTestHeaders, +}: { + initialTestHeaders: TestHeadersResult +}) { + const [testHeadersResult, setTestHeadersResult] = + Solid.createSignal(null) + + return ( +
+

Headers Test

+
{ + evt.preventDefault() + getTestHeaders().then(setTestHeadersResult) + }} + > + +
+
+

Initial Headers:

+
+          {JSON.stringify(initialTestHeaders.headers, null, 2)}
+        
+ {testHeadersResult() && ( + <> +

Updated Headers:

+
+              {JSON.stringify(testHeadersResult()?.headers, null, 2)}
+            
+ + )} +
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/index.tsx b/e2e/vue-start/server-functions/src/routes/index.tsx new file mode 100644 index 00000000000..155fb3dff1a --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/index.tsx @@ -0,0 +1,78 @@ +import { Link, createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return ( +
+

Server functions E2E tests

+
    +
  • + + Consistent server function returns both on client and server for GET + and POST calls + +
  • +
  • + + submitting multipart/form-data as server function input + +
  • +
  • + + Server function can return null for GET and POST calls + +
  • +
  • + + Server function can correctly send and receive FormData + +
  • +
  • + + server function can correctly send and receive headers + +
  • +
  • + + Direct POST submitting FormData to a Server function returns the + correct message + +
  • +
  • + + invoking a server function with custom response status code + +
  • +
  • + + isomorphic functions can have different implementations on client + and server + +
  • +
  • + + env-only functions can only be called on the server or client + respectively + +
  • +
  • + server function sets cookies +
  • +
  • + + dead code elimation only affects code after transformation + +
  • +
  • + aborting a server function call +
  • +
  • + server function returns raw response +
  • +
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx b/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx new file mode 100644 index 00000000000..955d671ea92 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx @@ -0,0 +1,79 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createIsomorphicFn, createServerFn } from '@tanstack/vue-start' +import { createSignal } from 'solid-js' + +const getEnv = createIsomorphicFn() + .server(() => 'server') + .client(() => 'client') + +const getServerEnv = createServerFn().handler(() => getEnv()) + +const getEcho = createIsomorphicFn() + .server((input: string) => 'server received ' + input) + .client((input) => 'client received ' + input) + +const getServerEcho = createServerFn() + .inputValidator((input: string) => input) + .handler(({ data }) => getEcho(data)) + +export const Route = createFileRoute('/isomorphic-fns')({ + component: RouteComponent, + loader() { + return { + envOnLoad: getEnv(), + } + }, +}) + +function RouteComponent() { + const loaderData = Route.useLoaderData() + const [results, setResults] = createSignal>>() + async function handleClick() { + const envOnClick = getEnv() + const echo = getEcho('hello') + const [serverEnv, serverEcho] = await Promise.all([ + getServerEnv(), + getServerEcho({ data: 'hello' }), + ]) + setResults({ envOnClick, echo, serverEnv, serverEcho }) + } + + return ( +
+ + {!!results() && ( +
+

+ getEnv +

+ When we called the function on the server it returned: +
+            {JSON.stringify(results()?.serverEnv)}
+          
+ When we called the function on the client it returned: +
+            {JSON.stringify(results()?.envOnClick)}
+          
+ When we called the function during SSR it returned: +
+            {JSON.stringify(loaderData().envOnLoad)}
+          
+
+

+ echo +

+ When we called the function on the server it returned: +
+            {JSON.stringify(results()?.serverEcho)}
+          
+ When we called the function on the client it returned: +
+            {JSON.stringify(results()?.echo)}
+          
+
+ )} +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx b/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx new file mode 100644 index 00000000000..39e889ae6ad --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx @@ -0,0 +1,79 @@ +import { createFileRoute, useRouter } from '@tanstack/vue-router' +import { + createMiddleware, + createServerFn, + getRouterInstance, +} from '@tanstack/vue-start' +import { createSignal } from 'solid-js' + +const middleware = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + const router = await getRouterInstance() + return next({ + sendContext: { + routerContext: router.options.context, + }, + }) + }, +) + +const serverFn = createServerFn() + .middleware([middleware]) + .handler(({ context }) => { + return context.routerContext + }) +export const Route = createFileRoute('/middleware/client-middleware-router')({ + component: RouteComponent, + loader: async () => ({ serverFnLoaderResult: await serverFn() }), +}) + +function RouteComponent() { + const [serverFnClientResult, setServerFnClientResult] = createSignal({}) + const loaderData = Route.useLoaderData() + + const router = useRouter() + return ( +
+

Client Middleware has access to router instance

+

+ This component checks that the client middleware has access to the + router instance and thus its context. +

+
+ It should return{' '} + +
+            {JSON.stringify(router.options.context)}
+          
+
+
+

+ serverFn when invoked in the loader returns: +
+ + {JSON.stringify(serverFnClientResult())} + +

+

+ serverFn when invoked on the client returns: +
+ + {JSON.stringify(loaderData().serverFnLoaderResult)} + +

+ +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/middleware/index.tsx b/e2e/vue-start/server-functions/src/routes/middleware/index.tsx new file mode 100644 index 00000000000..45c68fd1b12 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/middleware/index.tsx @@ -0,0 +1,37 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/middleware/')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

Server functions middleware E2E tests

+
    +
  • + + Client Middleware has access to router instance + +
  • +
  • + + Client Middleware can send server function reference in context + +
  • +
  • + + Request Middleware in combination with server function + +
  • +
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx b/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx new file mode 100644 index 00000000000..95959fe4ef4 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx @@ -0,0 +1,83 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { getRequest } from '@tanstack/vue-start/server' +import { createSignal, Show } from 'solid-js' + +const requestMiddleware = createMiddleware({ type: 'request' }).server( + async ({ next, request }) => { + return next({ + context: { + requestParam: request.url, + requestFunc: getRequest().url, + }, + }) + }, +) + +const serverFn = createServerFn() + .middleware([requestMiddleware]) + .handler(async ({ context: { requestParam, requestFunc } }) => { + return { requestParam, requestFunc } + }) + +export const Route = createFileRoute('/middleware/request-middleware')({ + loader: () => serverFn(), + component: RouteComponent, +}) + +function RouteComponent() { + const loaderData = Route.useLoaderData() + + const [clientData, setClientData] = createSignal | null>(null) + + return ( +
+

Request Middleware in combination with server function

+
+
+
+

Loader Data

Request Param: +
+ {loaderData().requestParam} +
+ Request Func: +
+ {loaderData().requestFunc} +
+
+
+
+ +
+
+
+

Client Data

+ + {(data) => ( +
+ Request Param: +
+ {data().requestParam} +
+ Request Func: +
+ {data().requestFunc} +
+
+ )} +
+
+
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx b/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx new file mode 100644 index 00000000000..00fd7185811 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx @@ -0,0 +1,78 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createMiddleware, createServerFn } from '@tanstack/vue-start' +import { createSignal } from 'solid-js' + +const middleware = createMiddleware({ type: 'function' }).client( + async ({ next }) => { + return next({ + sendContext: { + serverFn: barFn, + }, + }) + }, +) + +const fooFn = createServerFn() + .middleware([middleware]) + .handler(({ context }) => { + return context.serverFn() + }) +const barFn = createServerFn().handler(() => { + return 'bar' +}) + +export const Route = createFileRoute('/middleware/send-serverFn')({ + component: RouteComponent, + loader: async () => ({ serverFnLoaderResult: await fooFn() }), +}) + +function RouteComponent() { + const [serverFnClientResult, setServerFnClientResult] = createSignal({}) + const loaderData = Route.useLoaderData() + + return ( +
+

Send server function in context

+

+ This component checks that the client middleware can send a reference to + a server function in the context, which can then be invoked in the + server function handler. +

+
+ It should return{' '} + +
+            {JSON.stringify('bar')}
+          
+
+
+

+ serverFn when invoked in the loader returns: +
+ + {JSON.stringify(serverFnClientResult())} + +

+

+ serverFn when invoked on the client returns: +
+ + {JSON.stringify(loaderData().serverFnLoaderResult)} + +

+ +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/multipart.tsx b/e2e/vue-start/server-functions/src/routes/multipart.tsx new file mode 100644 index 00000000000..4ebda307603 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/multipart.tsx @@ -0,0 +1,107 @@ +import { createFileRoute } from '@tanstack/vue-router' +import * as Solid from 'solid-js' +import { createServerFn } from '@tanstack/vue-start' + +export const Route = createFileRoute('/multipart')({ + component: MultipartServerFnCall, +}) + +const multipartFormDataServerFn = createServerFn({ method: 'POST' }) + .inputValidator((x: unknown) => { + if (!(x instanceof FormData)) { + throw new Error('Invalid form data') + } + + const value = x.get('input_field') + const file = x.get('input_file') + + if (typeof value !== 'string') { + throw new Error('Submitted value is not a string') + } + + if (!(file instanceof File)) { + throw new Error('File is required') + } + + return { + submittedValue: value, + file, + } + }) + .handler(async ({ data }) => { + const contents = await data.file.text() + return { + value: data.submittedValue, + file: { + name: data.file.name, + size: data.file.size, + contents: contents, + }, + } + }) + +function MultipartServerFnCall() { + let formRef: HTMLFormElement | undefined + const [multipartResult, setMultipartResult] = Solid.createSignal({}) + + const handleSubmit = (e: any) => { + e.preventDefault() + + if (!formRef) { + return + } + + const formData = new FormData(formRef) + multipartFormDataServerFn({ data: formData }).then(setMultipartResult) + } + + return ( +
+

Multipart Server Fn POST Call

+
+ It should return{' '} + +
+            {JSON.stringify({
+              value: 'test field value',
+              file: { name: 'my_file.txt', size: 9, contents: 'test data' },
+            })}
+          
+
+
+
+ + + + +
+
+
+          {JSON.stringify(multipartResult())}
+        
+
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/primitives/index.tsx b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx new file mode 100644 index 00000000000..d8f938e2228 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx @@ -0,0 +1,132 @@ +import { useQuery } from '@tanstack/vue-query' +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn } from '@tanstack/vue-start' +import { For, Show } from 'solid-js' +import { z } from 'zod' +export const Route = createFileRoute('/primitives/')({ + component: RouteComponent, + ssr: true, +}) + +function stringify(data: any) { + return JSON.stringify(data === undefined ? '$undefined' : data) +} + +const $stringPost = createServerFn({ method: 'POST' }) + .inputValidator(z.string()) + .handler((ctx) => ctx.data) + +const $stringGet = createServerFn({ method: 'GET' }) + .inputValidator(z.string()) + .handler((ctx) => ctx.data) + +const $undefinedPost = createServerFn({ method: 'POST' }) + .inputValidator(z.undefined()) + .handler((ctx) => ctx.data) + +const $undefinedGet = createServerFn({ method: 'GET' }) + .inputValidator(z.undefined()) + .handler((ctx) => ctx.data) + +const $nullPost = createServerFn({ method: 'POST' }) + .inputValidator(z.null()) + .handler((ctx) => ctx.data) + +const $nullGet = createServerFn({ method: 'GET' }) + .inputValidator(z.null()) + .handler((ctx) => ctx.data) + +interface PrimitiveComponentProps { + serverFn: { + get: (opts: { data: T }) => Promise + post: (opts: { data: T }) => Promise + } + data: { + value: T + type: string + } +} + +interface TestProps extends PrimitiveComponentProps { + method: 'get' | 'post' +} +function Test(props: TestProps) { + const query = useQuery(() => ({ + queryKey: [props.data.type, props.method], + queryFn: async () => { + const result = await props.serverFn[props.method]({ + data: props.data.value, + }) + if (result === undefined) { + return '$undefined' + } + return result + }, + })) + const testId = `${props.method}-${props.data.type}` + return ( +
+

serverFn method={props.method}

+

expected

+
+ {stringify(props.data.value)} +
+

result

+ +
{stringify(query.data)}
+
+
+ ) +} +function PrimitiveComponent(props: PrimitiveComponentProps) { + return ( +
+

data type: {props.data.type}

+ +
+ +
+
+
+ ) +} + +function makeTestCase(props: PrimitiveComponentProps) { + return props +} +const testCases = [ + makeTestCase({ + data: { + value: null, + type: 'null', + }, + serverFn: { + get: $nullGet, + post: $nullPost, + }, + }), + makeTestCase({ + data: { + value: undefined, + type: 'undefined', + }, + serverFn: { + get: $undefinedGet, + post: $undefinedPost, + }, + }), + makeTestCase({ + data: { + value: 'foo-bar', + type: 'string', + }, + serverFn: { + get: $stringGet, + post: $stringPost, + }, + }), +] as Array> + +function RouteComponent() { + return {(t) => } +} diff --git a/e2e/vue-start/server-functions/src/routes/raw-response.tsx b/e2e/vue-start/server-functions/src/routes/raw-response.tsx new file mode 100644 index 00000000000..bf127b19c83 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/raw-response.tsx @@ -0,0 +1,47 @@ +import { createFileRoute } from '@tanstack/vue-router' +import * as Solid from 'solid-js' + +import { createServerFn } from '@tanstack/vue-start' + +export const Route = createFileRoute('/raw-response')({ + component: RouteComponent, +}) + +const expectedValue = 'Hello from a server function!' +export const rawResponseFn = createServerFn().handler(() => { + return new Response(expectedValue) +}) + +function RouteComponent() { + const [formDataResult, setFormDataResult] = Solid.createSignal({}) + + return ( +
+

Raw Response

+
+ It should return{' '} + +
{expectedValue}
+
+
+ + + +
+
{JSON.stringify(formDataResult())}
+
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx new file mode 100644 index 00000000000..2f8ef40f2a4 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/vue-query' +import { createFileRoute, redirect } from '@tanstack/vue-router' +import { createServerFn, useServerFn } from '@tanstack/vue-start' +import { Suspense } from 'solid-js' + +const $redirectServerFn = createServerFn({ method: 'GET' }).handler( + async () => { + throw redirect({ to: '/redirect-test-ssr/target' }) + }, +) + +export const Route = createFileRoute('/redirect-test-ssr/')({ + component: RouteComponent, + ssr: true, +}) + +function RouteComponent() { + const redirectFn = useServerFn($redirectServerFn) + const query = useQuery(() => ({ + queryKey: ['redirect-test-ssr'], + queryFn: () => redirectFn(), + })) + + return ( +
+

Redirect Source SSR

+ +
{JSON.stringify(query.data)}
+
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx new file mode 100644 index 00000000000..4f9584232ec --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/target.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/redirect-test-ssr/target')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

Redirect Target SSR

+

Successfully redirected!

+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx new file mode 100644 index 00000000000..0bfbb10a1d2 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/vue-query' +import { createFileRoute, redirect } from '@tanstack/vue-router' +import { createServerFn, useServerFn } from '@tanstack/vue-start' +import { Suspense } from 'solid-js' + +const $redirectServerFn = createServerFn({ method: 'GET' }).handler( + async () => { + throw redirect({ to: '/redirect-test/target' }) + }, +) + +export const Route = createFileRoute('/redirect-test/')({ + component: RouteComponent, + ssr: 'data-only', +}) + +function RouteComponent() { + const redirectFn = useServerFn($redirectServerFn) + const query = useQuery(() => ({ + queryKey: ['redirect-test'], + queryFn: () => redirectFn(), + })) + + return ( +
+

Redirect Source

+ +
{JSON.stringify(query.data)}
+
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx new file mode 100644 index 00000000000..10b25f0a153 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/redirect-test/target.tsx @@ -0,0 +1,14 @@ +import { createFileRoute } from '@tanstack/vue-router' + +export const Route = createFileRoute('/redirect-test/target')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( +
+

Redirect Target

+

Successfully redirected!

+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/return-null.tsx b/e2e/vue-start/server-functions/src/routes/return-null.tsx new file mode 100644 index 00000000000..764ba5f21dc --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/return-null.tsx @@ -0,0 +1,68 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn } from '@tanstack/vue-start' +import * as Solid from 'solid-js' + +/** + * This checks whether the server function can + * return null without throwing an error or returning something else. + * @link https://github.com/TanStack/router/issues/2776 + */ + +export const Route = createFileRoute('/return-null')({ + component: AllowServerFnReturnNull, +}) + +const $allow_return_null_getFn = createServerFn().handler(async () => { + return null +}) +const $allow_return_null_postFn = createServerFn({ method: 'POST' }).handler( + async () => { + return null + }, +) + +function AllowServerFnReturnNull() { + const [getServerResult, setGetServerResult] = Solid.createSignal('-') + const [postServerResult, setPostServerResult] = Solid.createSignal('-') + + return ( +
+

Allow ServerFn to return `null`

+

+ This component checks whether the server function can return null + without throwing an error. +

+
+ It should return{' '} + +
{JSON.stringify(null)}
+
+
+

+ {`GET: $allow_return_null_getFn returns`} +
+ + {JSON.stringify(getServerResult())} + +

+

+ {`POST: $allow_return_null_postFn returns`} +
+ + {JSON.stringify(postServerResult())} + +

+ +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx new file mode 100644 index 00000000000..4f46147d30b --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx @@ -0,0 +1,85 @@ +import { createFileRoute } from '@tanstack/vue-router' +import * as Solid from 'solid-js' + +import { createServerFn } from '@tanstack/vue-start' + +const testValues = { + name: 'Sean', + age: 25, + pet1: 'dog', + pet2: 'cat', + __adder: 1, +} + +export const greetUser = createServerFn({ method: 'POST' }) + .inputValidator((data: FormData) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid! FormData is required') + } + const name = data.get('name') + const age = data.get('age') + const pets = data.getAll('pet') + + if (!name || !age || pets.length === 0) { + throw new Error('Name, age and pets are required') + } + + return { + name: name.toString(), + age: parseInt(age.toString(), 10), + pets: pets.map((pet) => pet.toString()), + } + }) + .handler(({ data: { name, age, pets } }) => { + return `Hello, ${name}! You are ${age + testValues.__adder} years old, and your favorite pets are ${pets.join(',')}.` + }) + +export function SerializeFormDataFnCall() { + const [formDataResult, setFormDataResult] = Solid.createSignal({}) + + return ( +
+

Serialize FormData Fn POST Call

+
+ It should return{' '} + +
+            Hello, {testValues.name}! You are{' '}
+            {testValues.age + testValues.__adder} years old, and your favorite{' '}
+            pets are {testValues.pet1},{testValues.pet2}.
+          
+
+
+
{ + evt.preventDefault() + const data = new FormData(evt.currentTarget) + greetUser({ data }).then(setFormDataResult) + }} + > + + + + + +
+
+
+          {JSON.stringify(formDataResult())}
+        
+
+
+ ) +} + +export const Route = createFileRoute('/serialize-form-data')({ + component: SerializeFormDataFnCall, +}) diff --git a/e2e/vue-start/server-functions/src/routes/status.tsx b/e2e/vue-start/server-functions/src/routes/status.tsx new file mode 100644 index 00000000000..ba627bd3715 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/status.tsx @@ -0,0 +1,30 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn, useServerFn } from '@tanstack/vue-start' +import { setResponseStatus } from '@tanstack/vue-start/server' + +const helloFn = createServerFn().handler(() => { + setResponseStatus(225, `hello`) + return { + hello: 'world', + } +}) + +export const Route = createFileRoute('/status')({ + component: StatusComponent, +}) + +function StatusComponent() { + const hello = useServerFn(helloFn) + + return ( +
+ +
+ ) +} diff --git a/e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx b/e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx new file mode 100644 index 00000000000..35e083326d7 --- /dev/null +++ b/e2e/vue-start/server-functions/src/routes/submit-post-formdata.tsx @@ -0,0 +1,60 @@ +import { createFileRoute } from '@tanstack/vue-router' +import { createServerFn } from '@tanstack/vue-start' + +export const Route = createFileRoute('/submit-post-formdata')({ + component: SubmitPostFormDataFn, +}) + +const testValues = { + name: 'Sean', +} + +export const greetUser = createServerFn({ method: 'POST' }) + .inputValidator((data: FormData) => { + if (!(data instanceof FormData)) { + throw new Error('Invalid! FormData is required') + } + const name = data.get('name') + + if (!name) { + throw new Error('Name is required') + } + + return { + name: name.toString(), + } + }) + .handler(({ data: { name } }) => { + return new Response(`Hello, ${name}!`) + }) + +function SubmitPostFormDataFn() { + return ( +
+

Submit POST FormData Fn Call

+
+ It should return navigate and return{' '} + +
+            Hello, {testValues.name}!
+          
+
+
+
+ + +
+
+ ) +} diff --git a/e2e/vue-start/server-functions/src/styles/app.css b/e2e/vue-start/server-functions/src/styles/app.css new file mode 100644 index 00000000000..c36c737cd46 --- /dev/null +++ b/e2e/vue-start/server-functions/src/styles/app.css @@ -0,0 +1,30 @@ +@import 'tailwindcss'; + +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); + } +} + +@layer base { + html { + color-scheme: light dark; + } + + * { + @apply border-gray-200 dark:border-gray-800; + } + + html, + body { + @apply text-gray-900 bg-gray-50 dark:bg-gray-950 dark:text-gray-200; + } + + .using-mouse * { + outline: none !important; + } +} diff --git a/e2e/vue-start/server-functions/src/vite-env.d.ts b/e2e/vue-start/server-functions/src/vite-env.d.ts new file mode 100644 index 00000000000..0b2af560d60 --- /dev/null +++ b/e2e/vue-start/server-functions/src/vite-env.d.ts @@ -0,0 +1,4 @@ +declare module '*?url' { + const url: string + export default url +} diff --git a/e2e/vue-start/server-functions/tests/server-functions.spec.ts b/e2e/vue-start/server-functions/tests/server-functions.spec.ts new file mode 100644 index 00000000000..3b32deb5bdd --- /dev/null +++ b/e2e/vue-start/server-functions/tests/server-functions.spec.ts @@ -0,0 +1,497 @@ +import * as fs from 'node:fs' +import { expect } from '@playwright/test' +import { test } from '@tanstack/router-e2e-utils' +import { PORT } from '../playwright.config' +import type { Page } from '@playwright/test' + +test('Server function URLs correctly include constant ids', async ({ + page, +}) => { + for (const currentPage of ['/submit-post-formdata', '/formdata-redirect']) { + await page.goto(currentPage) + await page.waitForLoadState('networkidle') + + const form = page.locator('form') + const actionUrl = await form.getAttribute('action') + + expect(actionUrl).toMatch(/^\/_serverFn\/constant_id/) + } +}) + +test('invoking a server function with custom response status code', async ({ + page, +}) => { + await page.goto('/status') + + await page.waitForLoadState('networkidle') + + const requestPromise = new Promise((resolve) => { + page.on('response', (response) => { + expect(response.status()).toBe(225) + expect(response.statusText()).toBe('hello') + expect(response.headers()['content-type']).toContain('application/json') + resolve() + }) + }) + await page.getByTestId('invoke-server-fn').click() + await requestPromise +}) + +test('Consistent server function returns both on client and server for GET and POST calls', async ({ + page, +}) => { + await page.goto('/consistent') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-consistent-server-fns-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-consistent-server-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + // GET calls + await expect(page.getByTestId('cons_serverGetFn1-response')).toContainText( + expected, + ) + await expect(page.getByTestId('cons_getFn1-response')).toContainText(expected) + + // POST calls + await expect(page.getByTestId('cons_serverPostFn1-response')).toContainText( + expected, + ) + await expect(page.getByTestId('cons_postFn1-response')).toContainText( + expected, + ) +}) + +test('submitting multipart/form-data as server function input', async ({ + page, +}) => { + await page.goto('/multipart') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-multipart-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + const fileChooserPromise = page.waitForEvent('filechooser') + await page.getByTestId('multipart-form-file-input').click() + const fileChooser = await fileChooserPromise + await fileChooser.setFiles({ + name: 'my_file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('test data', 'utf-8'), + }) + await page.getByText('Submit (onClick)').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('multipart-form-response')).toContainText( + expected, + ) +}) + +test('isomorphic functions can have different implementations on client and server', async ({ + page, +}) => { + await page.goto('/isomorphic-fns') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-isomorphic-results-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('server-result')).toContainText('server') + await expect(page.getByTestId('client-result')).toContainText('client') + await expect(page.getByTestId('ssr-result')).toContainText('server') + + await expect(page.getByTestId('server-echo-result')).toContainText( + 'server received hello', + ) + await expect(page.getByTestId('client-echo-result')).toContainText( + 'client received hello', + ) +}) + +test('env-only functions can only be called on the server or client respectively', async ({ + page, +}) => { + await page.goto('/env-only') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('test-env-only-results-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('server-on-server')).toContainText( + 'server got: hello', + ) + await expect(page.getByTestId('server-on-client')).toContainText( + 'serverEcho threw an error: createServerOnlyFn() functions can only be called on the server!', + ) + + await expect(page.getByTestId('client-on-server')).toContainText( + 'clientEcho threw an error: createClientOnlyFn() functions can only be called on the client!', + ) + await expect(page.getByTestId('client-on-client')).toContainText( + 'client got: hello', + ) +}) + +test('Server function can return null for GET and POST calls', async ({ + page, +}) => { + await page.goto('/return-null') + + await page.waitForLoadState('networkidle') + await page.getByTestId('test-allow-server-fn-return-null-btn').click() + await page.waitForLoadState('networkidle') + + // GET call + await expect( + page.getByTestId('allow_return_null_getFn-response'), + ).toContainText(JSON.stringify(null)) + + // POST call + await expect( + page.getByTestId('allow_return_null_postFn-response'), + ).toContainText(JSON.stringify(null)) +}) + +test('Server function can correctly send and receive FormData', async ({ + page, +}) => { + await page.goto('/serialize-form-data') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-serialize-formdata-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-serialize-formdata-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + await expect( + page.getByTestId('serialize-formdata-form-response'), + ).toContainText(expected) +}) + +test('server function can correctly send and receive headers', async ({ + page, +}) => { + await page.goto('/headers') + + await page.waitForLoadState('networkidle') + let headers = JSON.parse( + await page.getByTestId('initial-headers-result').innerText(), + ) + expect(headers['host']).toBe(`localhost:${PORT}`) + expect(headers['user-agent']).toContain('Mozilla/5.0') + expect(headers['sec-fetch-mode']).toBe('navigate') + + await page.getByTestId('test-headers-btn').click() + await page.waitForSelector('[data-testid="updated-headers-result"]') + + headers = JSON.parse( + await page.getByTestId('updated-headers-result').innerText(), + ) + + expect(headers['host']).toBe(`localhost:${PORT}`) + expect(headers['user-agent']).toContain('Mozilla/5.0') + expect(headers['sec-fetch-mode']).toBe('cors') + expect(headers['referer']).toBe(`http://localhost:${PORT}/headers`) +}) + +test('Direct POST submitting FormData to a Server function returns the correct message', async ({ + page, +}) => { + await page.goto('/submit-post-formdata') + + await page.waitForLoadState('networkidle') + + const expected = + (await page + .getByTestId('expected-submit-post-formdata-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + const result = await page.innerText('body') + expect(result).toBe(expected) +}) + +test("server function's dead code is preserved if already there", async ({ + page, +}) => { + await page.goto('/dead-code-preserve') + + await page.waitForLoadState('networkidle') + await page.getByTestId('test-dead-code-fn-call-btn').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('dead-code-fn-call-response')).toContainText( + '1', + ) + + await fs.promises.rm('count-effect.txt') +}) + +test.describe('server function sets cookies', () => { + async function runCookieTest(page: Page, expectedCookieValue: string) { + for (let i = 1; i <= 4; i++) { + const key = `cookie-${i}-${expectedCookieValue}` + + const actualValue = await page.getByTestId(key).textContent() + expect(actualValue).toBe(expectedCookieValue) + } + } + test('SSR', async ({ page }) => { + const expectedCookieValue = `SSR-${Date.now()}` + await page.goto(`/cookies/set?value=${expectedCookieValue}`) + await runCookieTest(page, expectedCookieValue) + }) + + test('client side navigation', async ({ page }) => { + const expectedCookieValue = `CLIENT-${Date.now()}` + await page.goto(`/cookies?value=${expectedCookieValue}`) + await page.getByTestId('link-to-set').click() + await runCookieTest(page, expectedCookieValue) + }) +}) + +test.describe('aborting a server function call', () => { + test('without aborting', async ({ page }) => { + await page.goto('/abort-signal') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('run-without-abort-btn').click() + await page.waitForLoadState('networkidle') + await page.waitForSelector( + '[data-testid="result"]:has-text("server function result")', + ) + await page.waitForSelector( + '[data-testid="errorMessage"]:has-text("$undefined")', + ) + + const result = (await page.getByTestId('result').textContent()) || '' + expect(result).toBe('server function result') + + const errorMessage = + (await page.getByTestId('errorMessage').textContent()) || '' + expect(errorMessage).toBe('$undefined') + }) + + test('aborting', async ({ page }) => { + await page.goto('/abort-signal') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('run-with-abort-btn').click() + await page.waitForLoadState('networkidle') + await page.waitForSelector('[data-testid="result"]:has-text("$undefined")') + await page.waitForSelector( + '[data-testid="errorMessage"]:has-text("aborted")', + ) + + const result = (await page.getByTestId('result').textContent()) || '' + expect(result).toBe('$undefined') + + const errorMessage = + (await page.getByTestId('errorMessage').textContent()) || '' + expect(errorMessage).toContain('abort') + }) +}) + +test('raw response', async ({ page }) => { + await page.goto('/raw-response') + + await page.waitForLoadState('networkidle') + + const expectedValue = (await page.getByTestId('expected').textContent()) || '' + expect(expectedValue).not.toBe('') + + await page.getByTestId('button').click() + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('response')).toContainText(expectedValue) +}) + +test.describe('formdata redirect modes', () => { + for (const mode of ['js', 'no-js']) { + test(`Server function can redirect when sending formdata: mode = ${mode}`, async ({ + page, + }) => { + await page.goto('/formdata-redirect?mode=' + mode) + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-submit-post-formdata-server-fn-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click() + + await page.waitForLoadState('networkidle') + + await expect( + page.getByTestId('formdata-redirect-target-name'), + ).toContainText(expected) + + expect(page.url().endsWith(`/formdata-redirect/target/${expected}`)) + }) + } +}) + +test.describe('middleware', () => { + test.describe('client middleware should have access to router context via the router instance', () => { + async function runTest(page: Page) { + await page.waitForLoadState('networkidle') + + const expected = + (await page.getByTestId('expected-server-fn-result').textContent()) || + '' + expect(expected).not.toBe('') + + await page.getByTestId('btn-serverFn').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('serverFn-loader-result')).toContainText( + expected, + ) + await expect(page.getByTestId('serverFn-client-result')).toContainText( + expected, + ) + } + + test('direct visit', async ({ page }) => { + await page.goto('/middleware/client-middleware-router') + await runTest(page) + }) + + test('client navigation', async ({ page }) => { + await page.goto('/middleware') + await page.getByTestId('client-middleware-router-link').click() + await runTest(page) + }) + }) + + test('server function in combination with request middleware', async ({ + page, + }) => { + await page.goto('/middleware/request-middleware') + + await page.waitForLoadState('networkidle') + + async function checkEqual(prefix: string) { + const requestParam = await page + .getByTestId(`${prefix}-data-request-param`) + .textContent() + expect(requestParam).not.toBe('') + const requestFunc = await page + .getByTestId(`${prefix}-data-request-func`) + .textContent() + expect(requestParam).toBe(requestFunc) + } + + await checkEqual('loader') + + await page.getByTestId('client-call-button').click() + await page.waitForLoadState('networkidle') + + await checkEqual('client') + }) +}) + +test('factory', async ({ page }) => { + await page.goto('/factory') + + await expect(page.getByTestId('factory-route-component')).toBeInViewport() + + const buttons = await page + .locator('[data-testid^="btn-fn-"]') + .elementHandles() + for (const button of buttons) { + const testId = await button.getAttribute('data-testid') + + if (!testId) { + throw new Error('Button is missing data-testid') + } + + const suffix = testId.replace('btn-fn-', '') + + const expected = + (await page.getByTestId(`expected-fn-result-${suffix}`).textContent()) || + '' + expect(expected).not.toBe('') + + await button.click() + + await expect(page.getByTestId(`fn-result-${suffix}`)).toContainText( + expected, + ) + + await expect(page.getByTestId(`fn-comparison-${suffix}`)).toContainText( + 'equal', + ) + } +}) + +test('primitives', async ({ page }) => { + await page.goto('/primitives') + + await page.waitForLoadState('networkidle') + + // Wait for client-side hydration to complete + await expect(page.locator('[data-testid^="expected-"]').first()).toBeVisible() + + const testCases = await page + .locator('[data-testid^="expected-"]') + .elementHandles() + expect(testCases.length).not.toBe(0) + + for (const testCase of testCases) { + const testId = await testCase.getAttribute('data-testid') + + if (!testId) { + throw new Error('testcase is missing data-testid') + } + + const suffix = testId.replace('expected-', '') + + const expected = + (await page.getByTestId(`expected-${suffix}`).textContent()) || '' + expect(expected).not.toBe('') + + await expect(page.getByTestId(`result-${suffix}`)).toContainText(expected) + } +}) + +test('redirect in server function on direct navigation', async ({ page }) => { + // Test direct navigation to a route with a server function that redirects + await page.goto('/redirect-test') + + // Should redirect to target page + await expect(page.getByTestId('redirect-target')).toBeVisible() + expect(page.url()).toContain('/redirect-test/target') +}) + +test('redirect in server function called in query during SSR', async ({ + page, +}) => { + // Test direct navigation to a route with a server function that redirects + // when called inside a query with ssr: true + await page.goto('/redirect-test-ssr') + + // Should redirect to target page + await expect(page.getByTestId('redirect-target-ssr')).toBeVisible() + expect(page.url()).toContain('/redirect-test-ssr/target') +}) diff --git a/e2e/vue-start/server-functions/tsconfig.json b/e2e/vue-start/server-functions/tsconfig.json new file mode 100644 index 00000000000..a5ae5ae7e47 --- /dev/null +++ b/e2e/vue-start/server-functions/tsconfig.json @@ -0,0 +1,23 @@ +{ + "include": ["**/*.ts", "**/*.tsx", "public/script*.js"], + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "vue", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "isolatedModules": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "target": "ES2022", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + }, + "noEmit": true + } +} diff --git a/e2e/vue-start/server-functions/vite.config.ts b/e2e/vue-start/server-functions/vite.config.ts new file mode 100644 index 00000000000..cf6484d5bc2 --- /dev/null +++ b/e2e/vue-start/server-functions/vite.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/vue-start/plugin/vite' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +const FUNCTIONS_WITH_CONSTANT_ID = [ + 'src/routes/submit-post-formdata.tsx/greetUser_createServerFn_handler', + 'src/routes/formdata-redirect/index.tsx/greetUser_createServerFn_handler', +] + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths({ + projects: ['./tsconfig.json'], + }), + tanstackStart({ + serverFns: { + generateFunctionId: (opts) => { + const id = `${opts.filename}/${opts.functionName}` + if (FUNCTIONS_WITH_CONSTANT_ID.includes(id)) return 'constant_id' + else return undefined + }, + }, + }), + vue(), + vueJsx(), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 224c8db4305..74a5cb37b6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4965,6 +4965,82 @@ importers: specifier: ^3.1.8 version: 3.1.8(typescript@5.9.2) + e2e/vue-start/server-functions: + dependencies: + '@tanstack/vue-query': + specifier: ^5.90.9 + version: 5.92.0(vue@3.5.25(typescript@5.9.2)) + '@tanstack/vue-router': + specifier: workspace:* + version: link:../../../packages/vue-router + '@tanstack/vue-router-devtools': + specifier: workspace:* + version: link:../../../packages/vue-router-devtools + '@tanstack/vue-router-ssr-query': + specifier: workspace:* + version: link:../../../packages/vue-router-ssr-query + '@tanstack/vue-start': + specifier: workspace:* + version: link:../../../packages/vue-start + js-cookie: + specifier: ^3.0.5 + version: 3.0.5 + redaxios: + specifier: ^0.5.1 + version: 0.5.1 + tailwind-merge: + specifier: ^2.6.0 + version: 2.6.0 + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vue: + specifier: ^3.5.25 + version: 3.5.25(typescript@5.9.2) + zod: + specifier: ^3.24.2 + version: 3.25.57 + devDependencies: + '@playwright/test': + specifier: ^1.56.1 + version: 1.56.1 + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.15 + '@tanstack/router-e2e-utils': + specifier: workspace:^ + version: link:../../e2e-utils + '@types/js-cookie': + specifier: ^3.0.6 + version: 3.0.6 + '@types/node': + specifier: 22.10.2 + version: 22.10.2 + '@vitejs/plugin-vue': + specifier: ^6.0.3 + version: 6.0.3(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) + '@vitejs/plugin-vue-jsx': + specifier: ^5.1.2 + version: 5.1.2(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) + combinate: + specifier: ^1.1.11 + version: 1.1.11 + postcss: + specifier: ^8.5.1 + version: 8.5.6 + srvx: + specifier: ^0.8.6 + version: 0.8.15 + tailwindcss: + specifier: ^4.1.17 + version: 4.1.17 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + e2e/vue-start/server-routes: dependencies: '@tanstack/vue-query': From 5c8aa0ebd38e631b91b636093eeca9f872de01a5 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:05:58 +0100 Subject: [PATCH 02/12] port to vue --- .../src/components/DefaultCatchBoundary.tsx | 4 +- .../server-functions/src/routes/__root.tsx | 18 +- .../src/routes/abort-signal.tsx | 119 ++++++------ .../src/routes/consistent.tsx | 172 +++++++++--------- .../src/routes/cookies/index.tsx | 4 +- .../src/routes/cookies/set.tsx | 94 +++++----- .../src/routes/dead-code-preserve.tsx | 57 +++--- .../server-functions/src/routes/env-only.tsx | 112 ++++++------ .../src/routes/factory/index.tsx | 65 +++---- .../src/routes/formdata-redirect/index.tsx | 4 +- .../routes/formdata-redirect/target.$name.tsx | 2 +- .../server-functions/src/routes/headers.tsx | 105 ++++++----- .../src/routes/isomorphic-fns.tsx | 111 +++++------ .../middleware/client-middleware-router.tsx | 108 +++++------ .../routes/middleware/request-middleware.tsx | 93 +++++----- .../src/routes/middleware/send-serverFn.tsx | 108 +++++------ .../server-functions/src/routes/multipart.tsx | 134 +++++++------- .../src/routes/primitives/index.tsx | 120 ++++++------ .../src/routes/raw-response.tsx | 75 ++++---- .../src/routes/redirect-test-ssr/index.tsx | 42 +++-- .../src/routes/redirect-test/index.tsx | 42 +++-- .../src/routes/return-null.tsx | 102 ++++++----- .../src/routes/serialize-form-data.tsx | 90 ++++----- 23 files changed, 933 insertions(+), 848 deletions(-) diff --git a/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx b/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx index d6b3c732885..b1f818dd747 100644 --- a/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx +++ b/e2e/vue-start/server-functions/src/components/DefaultCatchBoundary.tsx @@ -28,7 +28,7 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) { > Try Again - {isRoot() ? ( + {isRoot.value ? ( { + onClick={(e: MouseEvent) => { e.preventDefault() window.history.back() }} diff --git a/e2e/vue-start/server-functions/src/routes/__root.tsx b/e2e/vue-start/server-functions/src/routes/__root.tsx index 329896a7c02..342f7a79b39 100644 --- a/e2e/vue-start/server-functions/src/routes/__root.tsx +++ b/e2e/vue-start/server-functions/src/routes/__root.tsx @@ -1,12 +1,13 @@ import { + Body, HeadContent, + Html, Outlet, Scripts, createRootRoute, } from '@tanstack/vue-router' -import { TanStackRouterDevtools } from '@tanstack/vue-router-devtools' -import { HydrationScript } from 'solid-js/web' +import { TanStackRouterDevtoolsInProd } from '@tanstack/vue-router-devtools' import { NotFound } from '~/components/NotFound' import appCss from '~/styles/app.css?url' @@ -32,16 +33,15 @@ export const Route = createRootRoute({ function RootComponent() { return ( - + - - - + + - + - - + + ) } diff --git a/e2e/vue-start/server-functions/src/routes/abort-signal.tsx b/e2e/vue-start/server-functions/src/routes/abort-signal.tsx index 4b3e6c969df..2a3e08ae593 100644 --- a/e2e/vue-start/server-functions/src/routes/abort-signal.tsx +++ b/e2e/vue-start/server-functions/src/routes/abort-signal.tsx @@ -1,10 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' import { createServerFn } from '@tanstack/vue-start' -import * as Solid from 'solid-js' - -export const Route = createFileRoute('/abort-signal')({ - component: RouteComponent, -}) +import { defineComponent, ref } from 'vue' const abortableServerFn = createServerFn().handler( async ({ context, signal }) => { @@ -27,59 +23,64 @@ const abortableServerFn = createServerFn().handler( }, ) -function RouteComponent() { - const [errorMessage, setErrorMessage] = Solid.createSignal< - string | undefined - >(undefined) - const [result, setResult] = Solid.createSignal(undefined) +const RouteComponent = defineComponent({ + setup() { + const errorMessage = ref(undefined) + const result = ref(undefined) - const reset = () => { - setErrorMessage(undefined) - setResult(undefined) - } - return ( -
- -
- -
- result:

{result() ?? '$undefined'}

-
-
- message:{' '} -

{errorMessage() ?? '$undefined'}

+ const reset = () => { + errorMessage.value = undefined + result.value = undefined + } + + return () => ( +
+ +
+ +
+ result:

{result.value ?? '$undefined'}

+
+
+ message:{' '} +

{errorMessage.value ?? '$undefined'}

+
-
- ) -} + ) + }, +}) + +export const Route = createFileRoute('/abort-signal')({ + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/consistent.tsx b/e2e/vue-start/server-functions/src/routes/consistent.tsx index 738c2c63242..5e6b0a7e08e 100644 --- a/e2e/vue-start/server-functions/src/routes/consistent.tsx +++ b/e2e/vue-start/server-functions/src/routes/consistent.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' -import * as Solid from 'solid-js' import { createServerFn } from '@tanstack/vue-start' +import { defineComponent, ref } from 'vue' /** * This checks whether the returned payloads from a @@ -10,15 +10,6 @@ import { createServerFn } from '@tanstack/vue-start' * @link https://github.com/TanStack/router/issues/2481 */ -export const Route = createFileRoute('/consistent')({ - component: ConsistentServerFnCalls, - loader: async () => { - const data = await cons_serverGetFn1({ data: { username: 'TEST' } }) - console.log('cons_serverGetFn1', data) - return { data } - }, -}) - const cons_getFn1 = createServerFn() .inputValidator((d: { username: string }) => d) .handler(({ data }) => { @@ -43,79 +34,96 @@ const cons_serverPostFn1 = createServerFn({ method: 'POST' }) return cons_postFn1({ data }) }) -function ConsistentServerFnCalls() { - const [getServerResult, setGetServerResult] = Solid.createSignal({}) - const [getDirectResult, setGetDirectResult] = Solid.createSignal({}) +const ConsistentServerFnCalls = defineComponent({ + setup() { + const getServerResult = ref({}) + const getDirectResult = ref({}) - const [postServerResult, setPostServerResult] = Solid.createSignal({}) - const [postDirectResult, setPostDirectResult] = Solid.createSignal({}) + const postServerResult = ref({}) + const postDirectResult = ref({}) - return ( -
-

Consistent Server Fn GET Calls

-

- This component checks whether the returned payloads from server function - are the same, regardless of whether the server function is called - directly from the client or from within the server function. -

-
- It should return{' '} - -
-            {JSON.stringify({ payload: { username: 'TEST' } })}
-          
-
-
-

- {`GET: cons_getFn1 called from server cons_serverGetFn1 returns`} -
- - {JSON.stringify(getServerResult())} - -

-

- {`GET: cons_getFn1 called directly returns`} -
- - {JSON.stringify(getDirectResult())} - -

-

- {`POST: cons_postFn1 called from cons_serverPostFn1 returns`} -
- - {JSON.stringify(postServerResult())} - -

-

- {`POST: cons_postFn1 called directly returns`} -
- - {JSON.stringify(postDirectResult())} - -

- +
+ ) + }, +}) - cons_postFn1({ data: { username: 'TEST' } }).then(setPostDirectResult) - }} - > - Test Consistent server function responses - -
- ) -} +export const Route = createFileRoute('/consistent')({ + component: ConsistentServerFnCalls, + loader: async () => { + const data = await cons_serverGetFn1({ data: { username: 'TEST' } }) + console.log('cons_serverGetFn1', data) + return { data } + }, +}) diff --git a/e2e/vue-start/server-functions/src/routes/cookies/index.tsx b/e2e/vue-start/server-functions/src/routes/cookies/index.tsx index 8a5f5a2a9c5..c47897c1096 100644 --- a/e2e/vue-start/server-functions/src/routes/cookies/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/cookies/index.tsx @@ -16,9 +16,9 @@ function RouteComponent() { data-testid="link-to-set" from="/cookies" to="./set" - search={search()} + search={search.value} > - got to route that sets the cookies with {JSON.stringify(search())} + got to route that sets the cookies with {JSON.stringify(search.value)} ) } diff --git a/e2e/vue-start/server-functions/src/routes/cookies/set.tsx b/e2e/vue-start/server-functions/src/routes/cookies/set.tsx index 5491704a406..39fd00b74e4 100644 --- a/e2e/vue-start/server-functions/src/routes/cookies/set.tsx +++ b/e2e/vue-start/server-functions/src/routes/cookies/set.tsx @@ -3,20 +3,10 @@ import { createServerFn } from '@tanstack/vue-start' import { setCookie } from '@tanstack/vue-start/server' import { z } from 'zod' import Cookies from 'js-cookie' -import * as Solid from 'solid-js' +import { defineComponent, ref, watch } from 'vue' const cookieSchema = z.object({ value: z.string() }) -export const Route = createFileRoute('/cookies/set')({ - validateSearch: cookieSchema, - loaderDeps: ({ search }) => search, - loader: async ({ deps }) => { - await setCookieServerFn1({ data: deps }) - await setCookieServerFn2({ data: deps }) - }, - component: RouteComponent, -}) - export const setCookieServerFn1 = createServerFn() .inputValidator(cookieSchema) .handler(({ data }) => { @@ -31,36 +21,58 @@ export const setCookieServerFn2 = createServerFn() setCookie(`cookie-4-${data.value}`, data.value) }) -function RouteComponent() { - const search = Route.useSearch() - const [cookiesFromDocument, setCookiesFromDocument] = Solid.createSignal< - Record | undefined - >(undefined) - Solid.createEffect(() => { - const tempCookies: Record = {} - for (let i = 1; i <= 4; i++) { - const key = `cookie-${i}-${search().value}` - tempCookies[key] = Cookies.get(key) +const RouteComponent = defineComponent({ + setup() { + const search = Route.useSearch() + const cookiesFromDocument = ref>({}) + + const updateCookies = () => { + const tempCookies: Record = {} + for (let i = 1; i <= 4; i++) { + const key = `cookie-${i}-${search.value.value}` + tempCookies[key] = Cookies.get(key) + } + cookiesFromDocument.value = tempCookies + } + + if (typeof window !== 'undefined') { + watch( + () => search.value.value, + () => { + updateCookies() + }, + { immediate: true }, + ) } - setCookiesFromDocument(tempCookies) - }, []) - return ( -
-

cookies result

- - - - - - - {Object.entries(cookiesFromDocument() || {}).map(([key, value]) => ( + + return () => ( +
+

cookies result

+
cookievalue
+ - - + + - ))} - -
{key}{value}cookievalue
-
- ) -} + {Object.entries(cookiesFromDocument.value).map(([key, value]) => ( + + {key} + {value} + + ))} + + + + ) + }, +}) + +export const Route = createFileRoute('/cookies/set')({ + validateSearch: cookieSchema, + loaderDeps: ({ search }) => search, + loader: async ({ deps }) => { + await setCookieServerFn1({ data: deps }) + await setCookieServerFn2({ data: deps }) + }, + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx index 0d3ae3b9682..aa06aca3036 100644 --- a/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx +++ b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx @@ -2,12 +2,7 @@ import { createFileRoute } from '@tanstack/vue-router' import * as fs from 'node:fs' import { createServerFn } from '@tanstack/vue-start' import { getRequestHeader } from '@tanstack/vue-start/server' -import { createSignal } from 'solid-js' -import {} from '@tanstack/vue-router' - -export const Route = createFileRoute('/dead-code-preserve')({ - component: RouteComponent, -}) +import { defineComponent, ref } from 'vue' // by using this we make sure DCE still works - this errors when imported on the client @@ -36,26 +31,30 @@ const readFileServerFn = createServerFn().handler(async () => { return data }) -function RouteComponent() { - const [serverFnOutput, setServerFnOutput] = createSignal() - return ( -
-

Dead code test

-

- This server function writes to a file as a side effect, then reads it. -

- -

Server output

-
{serverFnOutput()}
-
- ) -} +const RouteComponent = defineComponent({ + setup() { + const serverFnOutput = ref(undefined) + return () => ( +
+

Dead code test

+

This server function writes to a file as a side effect, then reads it.

+ +

Server output

+
{serverFnOutput.value}
+
+ ) + }, +}) + +export const Route = createFileRoute('/dead-code-preserve')({ + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/env-only.tsx b/e2e/vue-start/server-functions/src/routes/env-only.tsx index a1bdf6ae229..49bb03da8c3 100644 --- a/e2e/vue-start/server-functions/src/routes/env-only.tsx +++ b/e2e/vue-start/server-functions/src/routes/env-only.tsx @@ -4,7 +4,7 @@ import { createServerFn, createServerOnlyFn, } from '@tanstack/vue-start' -import { createSignal } from 'solid-js' +import { defineComponent, ref } from 'vue' const serverEcho = createServerOnlyFn((input: string) => 'server got: ' + input) const clientEcho = createClientOnlyFn((input: string) => 'client got: ' + input) @@ -22,56 +22,66 @@ const testOnServer = createServerFn().handler(() => { return { serverOnServer, clientOnServer } }) -export const Route = createFileRoute('/env-only')({ - component: RouteComponent, -}) +const RouteComponent = defineComponent({ + setup() { + const results = ref> | undefined>() -function RouteComponent() { - const [results, setResults] = createSignal>>() - - async function handleClick() { - const { serverOnServer, clientOnServer } = await testOnServer() - const clientOnClient = clientEcho('hello') - let serverOnClient: string - try { - serverOnClient = serverEcho('hello') - } catch (e) { - serverOnClient = - 'serverEcho threw an error: ' + - (e instanceof Error ? e.message : String(e)) + async function handleClick() { + const { serverOnServer, clientOnServer } = await testOnServer() + const clientOnClient = clientEcho('hello') + let serverOnClient: string + try { + serverOnClient = serverEcho('hello') + } catch (e) { + serverOnClient = + 'serverEcho threw an error: ' + + (e instanceof Error ? e.message : String(e)) + } + results.value = { + serverOnServer, + clientOnServer, + clientOnClient, + serverOnClient, + } } - setResults({ - serverOnServer, - clientOnServer, - clientOnClient, - serverOnClient, - }) - } - return ( -
- - {!!results() && ( -
-

- serverEcho -

- When we called the function on the server: -
{results()?.serverOnServer}
- When we called the function on the client: -
{results()?.serverOnClient}
-
-

- clientEcho -

- When we called the function on the server: -
{results()?.clientOnServer}
- When we called the function on the client: -
{results()?.clientOnClient}
-
- )} -
- ) -} + return () => ( +
+ + {!!results.value && ( +
+

+ serverEcho +

+ When we called the function on the server: +
+              {results.value.serverOnServer}
+            
+ When we called the function on the client: +
+              {results.value.serverOnClient}
+            
+
+

+ clientEcho +

+ When we called the function on the server: +
+              {results.value.clientOnServer}
+            
+ When we called the function on the client: +
+                {results.value.clientOnClient}
+              
+
+ )} +
+ ) + }, +}) + +export const Route = createFileRoute('/env-only')({ + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/factory/index.tsx b/e2e/vue-start/server-functions/src/routes/factory/index.tsx index 83301e263e0..eddb385a673 100644 --- a/e2e/vue-start/server-functions/src/routes/factory/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/factory/index.tsx @@ -1,6 +1,5 @@ import { createFileRoute, deepEqual } from '@tanstack/vue-router' -import { createSignal, For } from 'solid-js' import { createServerFn } from '@tanstack/vue-start' import { fooFnInsideFactoryFile } from './-functions/createFooServerFn' import { @@ -13,11 +12,7 @@ import { localFn, localFnPOST, } from './-functions/functions' - -export const Route = createFileRoute('/factory/')({ - ssr: false, - component: RouteComponent, -}) +import { computed, defineComponent, ref } from 'vue' const fnInsideRoute = createServerFn({ method: 'GET' }).handler(() => { return { @@ -137,17 +132,17 @@ interface TestCase { expected: any type: 'serverFn' | 'localFn' } -function Test(props: TestCase) { - const [result, setResult] = createSignal(null) - function comparison() { - if (result()) { - const isEqual = deepEqual(result(), props.expected) +const Test = defineComponent((props: TestCase) => { + const result = ref(null) + const comparison = computed(() => { + if (result.value) { + const isEqual = deepEqual(result.value, props.expected) return isEqual ? 'equal' : 'not equal' } return 'Loading...' - } + }) - return ( + return () => (
-            {props.type === 'serverFn'
-              ? JSON.stringify(props.expected)
-              : 'localFn'}
+            {props.type === 'serverFn' ? JSON.stringify(props.expected) : 'localFn'}
           
@@ -167,14 +160,14 @@ function Test(props: TestCase) { fn returns:
- {result() + {result.value ? props.type === 'serverFn' - ? JSON.stringify(result()) + ? JSON.stringify(result.value) : 'localFn' : 'Loading...'} {' '} - {comparison()} + {comparison.value}

) -} -function RouteComponent() { - return ( -
-

Server functions middleware E2E tests

- - {([name, testCase]) => } - -
- ) -} +}) + +const RouteComponent = defineComponent({ + setup() { + return () => ( +
+

Server functions middleware E2E tests

+ {Object.entries(functions).map(([name, testCase]) => ( + + ))} +
+ ) + }, +}) + +export const Route = createFileRoute('/factory/')({ + ssr: false, + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx index 09eac5f1f64..d23eb342b3f 100644 --- a/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/index.tsx @@ -53,9 +53,9 @@ function SubmitPostFormDataFn() { method="post" action={greetUser.url} onSubmit={async (evt) => { - if (mode() === 'js') { + if (mode.value === 'js') { evt.preventDefault() - const data = new FormData(evt.currentTarget) + const data = new FormData(evt.currentTarget as HTMLFormElement) await greetUserFn({ data }) } }} diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx index 9d2595533d7..7c9f682d165 100644 --- a/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx +++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx @@ -9,7 +9,7 @@ function RouteComponent() { return (
Hello{' '} - {params().name}! + {params.value.name}!
) } diff --git a/e2e/vue-start/server-functions/src/routes/headers.tsx b/e2e/vue-start/server-functions/src/routes/headers.tsx index 805f5b1f08a..9cb3d7533c7 100644 --- a/e2e/vue-start/server-functions/src/routes/headers.tsx +++ b/e2e/vue-start/server-functions/src/routes/headers.tsx @@ -1,23 +1,11 @@ import { createFileRoute } from '@tanstack/vue-router' -import * as Solid from 'solid-js' import { createServerFn } from '@tanstack/vue-start' import { getRequestHeaders, setResponseHeader, } from '@tanstack/vue-start/server' import type { RequestHeaderName } from '@tanstack/vue-start/server' - -export const Route = createFileRoute('/headers')({ - loader: async () => { - return { - testHeaders: await getTestHeaders(), - } - }, - component: () => { - const loaderData = Route.useLoaderData() - return - }, -}) +import { defineComponent, ref } from 'vue' export const getTestHeaders = createServerFn().handler(() => { setResponseHeader('x-test-header', 'test-value') @@ -34,47 +22,56 @@ type TestHeadersResult = { serverHeaders?: Partial> } -function ResponseHeaders({ - initialTestHeaders, -}: { - initialTestHeaders: TestHeadersResult -}) { - const [testHeadersResult, setTestHeadersResult] = - Solid.createSignal(null) +const HeadersRouteComponent = defineComponent({ + setup() { + const loaderData = Route.useLoaderData() + const testHeadersResult = ref(null) - return ( -
-

Headers Test

-
{ - evt.preventDefault() - getTestHeaders().then(setTestHeadersResult) - }} - > - -
-
-

Initial Headers:

-
-          {JSON.stringify(initialTestHeaders.headers, null, 2)}
-        
- {testHeadersResult() && ( - <> -

Updated Headers:

-
-              {JSON.stringify(testHeadersResult()?.headers, null, 2)}
-            
- - )} + + +
+

Initial Headers:

+
+            {JSON.stringify(loaderData.value.testHeaders.headers, null, 2)}
+          
+ {testHeadersResult.value && ( + <> +

Updated Headers:

+
+                {JSON.stringify(testHeadersResult.value.headers, null, 2)}
+              
+ + )} +
-
- ) -} + ) + }, +}) + +export const Route = createFileRoute('/headers')({ + loader: async () => { + return { + testHeaders: await getTestHeaders(), + } + }, + component: HeadersRouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx b/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx index 955d671ea92..6d0679e34dc 100644 --- a/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx +++ b/e2e/vue-start/server-functions/src/routes/isomorphic-fns.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' import { createIsomorphicFn, createServerFn } from '@tanstack/vue-start' -import { createSignal } from 'solid-js' +import { defineComponent, ref } from 'vue' const getEnv = createIsomorphicFn() .server(() => 'server') @@ -16,6 +16,62 @@ const getServerEcho = createServerFn() .inputValidator((input: string) => input) .handler(({ data }) => getEcho(data)) +const RouteComponent = defineComponent({ + setup() { + const loaderData = Route.useLoaderData() + const results = ref> | undefined>() + + async function handleClick() { + const envOnClick = getEnv() + const echo = getEcho('hello') + const [serverEnv, serverEcho] = await Promise.all([ + getServerEnv(), + getServerEcho({ data: 'hello' }), + ]) + results.value = { envOnClick, echo, serverEnv, serverEcho } + } + + return () => ( +
+ + {!!results.value && ( +
+

+ getEnv +

+ When we called the function on the server it returned: +
+              {JSON.stringify(results.value.serverEnv)}
+            
+ When we called the function on the client it returned: +
+              {JSON.stringify(results.value.envOnClick)}
+            
+ When we called the function during SSR it returned: +
+              {JSON.stringify(loaderData.value.envOnLoad)}
+            
+
+

+ echo +

+ When we called the function on the server it returned: +
+              {JSON.stringify(results.value.serverEcho)}
+            
+ When we called the function on the client it returned: +
+              {JSON.stringify(results.value.echo)}
+            
+
+ )} +
+ ) + }, +}) + export const Route = createFileRoute('/isomorphic-fns')({ component: RouteComponent, loader() { @@ -24,56 +80,3 @@ export const Route = createFileRoute('/isomorphic-fns')({ } }, }) - -function RouteComponent() { - const loaderData = Route.useLoaderData() - const [results, setResults] = createSignal>>() - async function handleClick() { - const envOnClick = getEnv() - const echo = getEcho('hello') - const [serverEnv, serverEcho] = await Promise.all([ - getServerEnv(), - getServerEcho({ data: 'hello' }), - ]) - setResults({ envOnClick, echo, serverEnv, serverEcho }) - } - - return ( -
- - {!!results() && ( -
-

- getEnv -

- When we called the function on the server it returned: -
-            {JSON.stringify(results()?.serverEnv)}
-          
- When we called the function on the client it returned: -
-            {JSON.stringify(results()?.envOnClick)}
-          
- When we called the function during SSR it returned: -
-            {JSON.stringify(loaderData().envOnLoad)}
-          
-
-

- echo -

- When we called the function on the server it returned: -
-            {JSON.stringify(results()?.serverEcho)}
-          
- When we called the function on the client it returned: -
-            {JSON.stringify(results()?.echo)}
-          
-
- )} -
- ) -} diff --git a/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx b/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx index 39e889ae6ad..b13fb972fa4 100644 --- a/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx +++ b/e2e/vue-start/server-functions/src/routes/middleware/client-middleware-router.tsx @@ -4,7 +4,7 @@ import { createServerFn, getRouterInstance, } from '@tanstack/vue-start' -import { createSignal } from 'solid-js' +import { defineComponent, ref } from 'vue' const middleware = createMiddleware({ type: 'function' }).client( async ({ next }) => { @@ -22,58 +22,62 @@ const serverFn = createServerFn() .handler(({ context }) => { return context.routerContext }) +const RouteComponent = defineComponent({ + setup() { + const serverFnClientResult = ref({}) + const loaderData = Route.useLoaderData() + const router = useRouter() + + return () => ( +
+

Client Middleware has access to router instance

+

+ This component checks that the client middleware has access to the + router instance and thus its context. +

+
+ It should return{' '} + +
+              {JSON.stringify(router.options.context)}
+            
+
+
+

+ serverFn when invoked in the loader returns: +
+ + {JSON.stringify(serverFnClientResult.value)} + +

+

+ serverFn when invoked on the client returns: +
+ + {JSON.stringify(loaderData.value.serverFnLoaderResult)} + +

+ +
+ ) + }, +}) + export const Route = createFileRoute('/middleware/client-middleware-router')({ component: RouteComponent, loader: async () => ({ serverFnLoaderResult: await serverFn() }), }) - -function RouteComponent() { - const [serverFnClientResult, setServerFnClientResult] = createSignal({}) - const loaderData = Route.useLoaderData() - - const router = useRouter() - return ( -
-

Client Middleware has access to router instance

-

- This component checks that the client middleware has access to the - router instance and thus its context. -

-
- It should return{' '} - -
-            {JSON.stringify(router.options.context)}
-          
-
-
-

- serverFn when invoked in the loader returns: -
- - {JSON.stringify(serverFnClientResult())} - -

-

- serverFn when invoked on the client returns: -
- - {JSON.stringify(loaderData().serverFnLoaderResult)} - -

- -
- ) -} diff --git a/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx b/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx index 95959fe4ef4..63c0fe2b748 100644 --- a/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx +++ b/e2e/vue-start/server-functions/src/routes/middleware/request-middleware.tsx @@ -1,7 +1,7 @@ import { createFileRoute } from '@tanstack/vue-router' import { createMiddleware, createServerFn } from '@tanstack/vue-start' import { getRequest } from '@tanstack/vue-start/server' -import { createSignal, Show } from 'solid-js' +import { defineComponent, ref } from 'vue' const requestMiddleware = createMiddleware({ type: 'request' }).server( async ({ next, request }) => { @@ -20,64 +20,65 @@ const serverFn = createServerFn() return { requestParam, requestFunc } }) -export const Route = createFileRoute('/middleware/request-middleware')({ - loader: () => serverFn(), - component: RouteComponent, -}) - -function RouteComponent() { - const loaderData = Route.useLoaderData() +type ServerFnResult = Awaited> - const [clientData, setClientData] = createSignal | null>(null) +const RouteComponent = defineComponent({ + setup() { + const loaderData = Route.useLoaderData() + const clientData = ref(null) - return ( -
-

Request Middleware in combination with server function

-
+ return () => (
-
-

Loader Data

Request Param: -
- {loaderData().requestParam} +

Request Middleware in combination with server function

+
+
+
+

Loader Data

Request Param: +
+ {loaderData.value.requestParam} +
+ Request Func: +
+ {loaderData.value.requestFunc} +
- Request Func: -
- {loaderData().requestFunc} +
+
+
-
-
-
- -
-
-
-

Client Data

- - {(data) => ( +
+
+

Client Data

+ {clientData.value ? (
Request Param:
- {data().requestParam} + {clientData.value.requestParam}
Request Func:
- {data().requestFunc} + {clientData.value.requestFunc}
+ ) : ( + ' Loading ...' )} - +
-
- ) -} + ) + }, +}) + +export const Route = createFileRoute('/middleware/request-middleware')({ + loader: () => serverFn(), + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx b/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx index 00fd7185811..ff6ef46efb4 100644 --- a/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx +++ b/e2e/vue-start/server-functions/src/routes/middleware/send-serverFn.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' import { createMiddleware, createServerFn } from '@tanstack/vue-start' -import { createSignal } from 'solid-js' +import { defineComponent, ref } from 'vue' const middleware = createMiddleware({ type: 'function' }).client( async ({ next }) => { @@ -21,58 +21,62 @@ const barFn = createServerFn().handler(() => { return 'bar' }) +const RouteComponent = defineComponent({ + setup() { + const serverFnClientResult = ref({}) + const loaderData = Route.useLoaderData() + + return () => ( +
+

Send server function in context

+

+ This component checks that the client middleware can send a reference + to a server function in the context, which can then be invoked in the + server function handler. +

+
+ It should return{' '} + +
+              {JSON.stringify('bar')}
+            
+
+
+

+ serverFn when invoked in the loader returns: +
+ + {JSON.stringify(serverFnClientResult.value)} + +

+

+ serverFn when invoked on the client returns: +
+ + {JSON.stringify(loaderData.value.serverFnLoaderResult)} + +

+ +
+ ) + }, +}) + export const Route = createFileRoute('/middleware/send-serverFn')({ component: RouteComponent, loader: async () => ({ serverFnLoaderResult: await fooFn() }), }) - -function RouteComponent() { - const [serverFnClientResult, setServerFnClientResult] = createSignal({}) - const loaderData = Route.useLoaderData() - - return ( -
-

Send server function in context

-

- This component checks that the client middleware can send a reference to - a server function in the context, which can then be invoked in the - server function handler. -

-
- It should return{' '} - -
-            {JSON.stringify('bar')}
-          
-
-
-

- serverFn when invoked in the loader returns: -
- - {JSON.stringify(serverFnClientResult())} - -

-

- serverFn when invoked on the client returns: -
- - {JSON.stringify(loaderData().serverFnLoaderResult)} - -

- -
- ) -} diff --git a/e2e/vue-start/server-functions/src/routes/multipart.tsx b/e2e/vue-start/server-functions/src/routes/multipart.tsx index 4ebda307603..c41fd8e4e58 100644 --- a/e2e/vue-start/server-functions/src/routes/multipart.tsx +++ b/e2e/vue-start/server-functions/src/routes/multipart.tsx @@ -1,10 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' -import * as Solid from 'solid-js' import { createServerFn } from '@tanstack/vue-start' - -export const Route = createFileRoute('/multipart')({ - component: MultipartServerFnCall, -}) +import { defineComponent, ref } from 'vue' const multipartFormDataServerFn = createServerFn({ method: 'POST' }) .inputValidator((x: unknown) => { @@ -40,68 +36,78 @@ const multipartFormDataServerFn = createServerFn({ method: 'POST' }) } }) -function MultipartServerFnCall() { - let formRef: HTMLFormElement | undefined - const [multipartResult, setMultipartResult] = Solid.createSignal({}) +const MultipartServerFnCall = defineComponent({ + setup() { + const formRef = ref(null) + const multipartResult = ref({}) - const handleSubmit = (e: any) => { - e.preventDefault() + const handleSubmit = (e: Event) => { + e.preventDefault() - if (!formRef) { - return - } + if (!formRef.value) { + return + } - const formData = new FormData(formRef) - multipartFormDataServerFn({ data: formData }).then(setMultipartResult) - } + const formData = new FormData(formRef.value) + multipartFormDataServerFn({ data: formData }).then((data) => { + multipartResult.value = data + }) + } - return ( -
-

Multipart Server Fn POST Call

-
- It should return{' '} - -
-            {JSON.stringify({
-              value: 'test field value',
-              file: { name: 'my_file.txt', size: 9, contents: 'test data' },
-            })}
-          
-
-
-
- - - - -
-
-
-          {JSON.stringify(multipartResult())}
-        
+ + + + + +
+
+            {JSON.stringify(multipartResult.value)}
+          
+
-
- ) -} + ) + }, +}) + +export const Route = createFileRoute('/multipart')({ + component: MultipartServerFnCall, +}) diff --git a/e2e/vue-start/server-functions/src/routes/primitives/index.tsx b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx index d8f938e2228..273ceff93a9 100644 --- a/e2e/vue-start/server-functions/src/routes/primitives/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx @@ -1,12 +1,8 @@ import { useQuery } from '@tanstack/vue-query' import { createFileRoute } from '@tanstack/vue-router' import { createServerFn } from '@tanstack/vue-start' -import { For, Show } from 'solid-js' +import { defineComponent } from 'vue' import { z } from 'zod' -export const Route = createFileRoute('/primitives/')({ - component: RouteComponent, - ssr: true, -}) function stringify(data: any) { return JSON.stringify(data === undefined ? '$undefined' : data) @@ -47,50 +43,6 @@ interface PrimitiveComponentProps { } } -interface TestProps extends PrimitiveComponentProps { - method: 'get' | 'post' -} -function Test(props: TestProps) { - const query = useQuery(() => ({ - queryKey: [props.data.type, props.method], - queryFn: async () => { - const result = await props.serverFn[props.method]({ - data: props.data.value, - }) - if (result === undefined) { - return '$undefined' - } - return result - }, - })) - const testId = `${props.method}-${props.data.type}` - return ( -
-

serverFn method={props.method}

-

expected

-
- {stringify(props.data.value)} -
-

result

- -
{stringify(query.data)}
-
-
- ) -} -function PrimitiveComponent(props: PrimitiveComponentProps) { - return ( -
-

data type: {props.data.type}

- -
- -
-
-
- ) -} - function makeTestCase(props: PrimitiveComponentProps) { return props } @@ -127,6 +79,70 @@ const testCases = [ }), ] as Array> -function RouteComponent() { - return {(t) => } -} +type Method = 'get' | 'post' + +const RouteComponent = defineComponent({ + setup() { + const testQueries = testCases.map((testCase) => { + const makeQuery = (method: Method) => + useQuery(() => ({ + queryKey: [testCase.data.type, method], + queryFn: async () => { + const result = await testCase.serverFn[method]({ + data: testCase.data.value, + }) + if (result === undefined) { + return '$undefined' + } + return result + }, + })) + + return { + testCase, + queries: { + post: makeQuery('post'), + get: makeQuery('get'), + }, + } + }) + + return () => ( + <> + {testQueries.map(({ testCase, queries }) => ( +
+

data type: {testCase.data.type}

+ + {(['post', 'get'] as const).map((method) => { + const testId = `${method}-${testCase.data.type}` + const query = queries[method] + return ( +
+

serverFn method={method}

+

expected

+
+ {stringify(testCase.data.value)} +
+

result

+
+ {query.isSuccess.value + ? stringify(query.data.value) + : ''} +
+
+
+ ) + })} +
+
+
+ ))} + + ) + }, +}) + +export const Route = createFileRoute('/primitives/')({ + component: RouteComponent, + ssr: true, +}) diff --git a/e2e/vue-start/server-functions/src/routes/raw-response.tsx b/e2e/vue-start/server-functions/src/routes/raw-response.tsx index bf127b19c83..e01b56e70d8 100644 --- a/e2e/vue-start/server-functions/src/routes/raw-response.tsx +++ b/e2e/vue-start/server-functions/src/routes/raw-response.tsx @@ -1,47 +1,48 @@ import { createFileRoute } from '@tanstack/vue-router' -import * as Solid from 'solid-js' - import { createServerFn } from '@tanstack/vue-start' - -export const Route = createFileRoute('/raw-response')({ - component: RouteComponent, -}) +import { defineComponent, ref } from 'vue' const expectedValue = 'Hello from a server function!' export const rawResponseFn = createServerFn().handler(() => { return new Response(expectedValue) }) -function RouteComponent() { - const [formDataResult, setFormDataResult] = Solid.createSignal({}) - - return ( -
-

Raw Response

-
- It should return{' '} - -
{expectedValue}
-
+const RouteComponent = defineComponent({ + setup() { + const formDataResult = ref('') + + return () => ( +
+

Raw Response

+
+ It should return{' '} + +
{expectedValue}
+
+
+ + + +
+
{JSON.stringify(formDataResult.value)}
+
+ ) + }, +}) - - -
-
{JSON.stringify(formDataResult())}
-
-
- ) -} +export const Route = createFileRoute('/raw-response')({ + component: RouteComponent, +}) diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx index 2f8ef40f2a4..46d6efcefd4 100644 --- a/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/redirect-test-ssr/index.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/vue-query' import { createFileRoute, redirect } from '@tanstack/vue-router' import { createServerFn, useServerFn } from '@tanstack/vue-start' -import { Suspense } from 'solid-js' +import { Suspense, defineComponent } from 'vue' const $redirectServerFn = createServerFn({ method: 'GET' }).handler( async () => { @@ -9,24 +9,30 @@ const $redirectServerFn = createServerFn({ method: 'GET' }).handler( }, ) +const RouteComponent = defineComponent({ + setup() { + const redirectFn = useServerFn($redirectServerFn) + const query = useQuery(() => ({ + queryKey: ['redirect-test-ssr'], + queryFn: () => redirectFn(), + suspense: true, + })) + + return () => ( +
+

Redirect Source SSR

+ + {{ + default: () =>
{JSON.stringify(query.data.value)}
, + fallback: () =>
Loading...
, + }} +
+
+ ) + }, +}) + export const Route = createFileRoute('/redirect-test-ssr/')({ component: RouteComponent, ssr: true, }) - -function RouteComponent() { - const redirectFn = useServerFn($redirectServerFn) - const query = useQuery(() => ({ - queryKey: ['redirect-test-ssr'], - queryFn: () => redirectFn(), - })) - - return ( -
-

Redirect Source SSR

- -
{JSON.stringify(query.data)}
-
-
- ) -} diff --git a/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx b/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx index 0bfbb10a1d2..87df7fa5b5e 100644 --- a/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/redirect-test/index.tsx @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/vue-query' import { createFileRoute, redirect } from '@tanstack/vue-router' import { createServerFn, useServerFn } from '@tanstack/vue-start' -import { Suspense } from 'solid-js' +import { Suspense, defineComponent } from 'vue' const $redirectServerFn = createServerFn({ method: 'GET' }).handler( async () => { @@ -9,24 +9,30 @@ const $redirectServerFn = createServerFn({ method: 'GET' }).handler( }, ) +const RouteComponent = defineComponent({ + setup() { + const redirectFn = useServerFn($redirectServerFn) + const query = useQuery(() => ({ + queryKey: ['redirect-test'], + queryFn: () => redirectFn(), + suspense: true, + })) + + return () => ( +
+

Redirect Source

+ + {{ + default: () =>
{JSON.stringify(query.data.value)}
, + fallback: () =>
Loading...
, + }} +
+
+ ) + }, +}) + export const Route = createFileRoute('/redirect-test/')({ component: RouteComponent, ssr: 'data-only', }) - -function RouteComponent() { - const redirectFn = useServerFn($redirectServerFn) - const query = useQuery(() => ({ - queryKey: ['redirect-test'], - queryFn: () => redirectFn(), - })) - - return ( -
-

Redirect Source

- -
{JSON.stringify(query.data)}
-
-
- ) -} diff --git a/e2e/vue-start/server-functions/src/routes/return-null.tsx b/e2e/vue-start/server-functions/src/routes/return-null.tsx index 764ba5f21dc..de88ad71138 100644 --- a/e2e/vue-start/server-functions/src/routes/return-null.tsx +++ b/e2e/vue-start/server-functions/src/routes/return-null.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' import { createServerFn } from '@tanstack/vue-start' -import * as Solid from 'solid-js' +import { defineComponent, ref } from 'vue' /** * This checks whether the server function can @@ -8,10 +8,6 @@ import * as Solid from 'solid-js' * @link https://github.com/TanStack/router/issues/2776 */ -export const Route = createFileRoute('/return-null')({ - component: AllowServerFnReturnNull, -}) - const $allow_return_null_getFn = createServerFn().handler(async () => { return null }) @@ -21,48 +17,58 @@ const $allow_return_null_postFn = createServerFn({ method: 'POST' }).handler( }, ) -function AllowServerFnReturnNull() { - const [getServerResult, setGetServerResult] = Solid.createSignal('-') - const [postServerResult, setPostServerResult] = Solid.createSignal('-') +const AllowServerFnReturnNull = defineComponent({ + setup() { + const getServerResult = ref('-') + const postServerResult = ref('-') - return ( -
-

Allow ServerFn to return `null`

-

- This component checks whether the server function can return null - without throwing an error. -

-
- It should return{' '} - -
{JSON.stringify(null)}
-
+ return () => ( +
+

Allow ServerFn to return `null`

+

+ This component checks whether the server function can return null + without throwing an error. +

+
+ It should return{' '} + +
{JSON.stringify(null)}
+
+
+

+ {`GET: $allow_return_null_getFn returns`} +
+ + {JSON.stringify(getServerResult.value)} + +

+

+ {`POST: $allow_return_null_postFn returns`} +
+ + {JSON.stringify(postServerResult.value)} + +

+
-

- {`GET: $allow_return_null_getFn returns`} -
- - {JSON.stringify(getServerResult())} - -

-

- {`POST: $allow_return_null_postFn returns`} -
- - {JSON.stringify(postServerResult())} - -

- -
- ) -} + ) + }, +}) + +export const Route = createFileRoute('/return-null')({ + component: AllowServerFnReturnNull, +}) diff --git a/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx index 4f46147d30b..cc0af7012c2 100644 --- a/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx +++ b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx @@ -1,7 +1,6 @@ import { createFileRoute } from '@tanstack/vue-router' -import * as Solid from 'solid-js' - import { createServerFn } from '@tanstack/vue-start' +import { defineComponent, ref } from 'vue' const testValues = { name: 'Sean', @@ -34,51 +33,54 @@ export const greetUser = createServerFn({ method: 'POST' }) return `Hello, ${name}! You are ${age + testValues.__adder} years old, and your favorite pets are ${pets.join(',')}.` }) -export function SerializeFormDataFnCall() { - const [formDataResult, setFormDataResult] = Solid.createSignal({}) +export const SerializeFormDataFnCall = defineComponent({ + setup() { + const formDataResult = ref('') - return ( -
-

Serialize FormData Fn POST Call

-
- It should return{' '} - -
-            Hello, {testValues.name}! You are{' '}
-            {testValues.age + testValues.__adder} years old, and your favorite{' '}
-            pets are {testValues.pet1},{testValues.pet2}.
-          
-
-
-
{ - evt.preventDefault() - const data = new FormData(evt.currentTarget) - greetUser({ data }).then(setFormDataResult) - }} - > - - - - - -
-
-
-          {JSON.stringify(formDataResult())}
-        
+ + + + + + +
+
+            {JSON.stringify(formDataResult.value)}
+          
+
-
- ) -} + ) + }, +}) export const Route = createFileRoute('/serialize-form-data')({ component: SerializeFormDataFnCall, From 02167f42b0ce8ecb6178bee7e45b1381d1ae7f1e Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:25:54 +0100 Subject: [PATCH 03/12] fix vue header image --- packages/vue-router/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-router/README.md b/packages/vue-router/README.md index aef2489ce02..25c561a896e 100644 --- a/packages/vue-router/README.md +++ b/packages/vue-router/README.md @@ -2,7 +2,7 @@ # TanStack Vue Router -![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header.png) +![TanStack Router Header](https://github.com/tanstack/router/raw/main/media/header_router.png) 🤖 Type-safe router w/ built-in caching & URL state management for Vue! From 33f8f98d9b44a3c8434f99849a90f5e09d9cc30c Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:27:48 +0100 Subject: [PATCH 04/12] use "srvx": "^0.9.8", --- e2e/vue-start/server-functions/package.json | 2 +- pnpm-lock.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/vue-start/server-functions/package.json b/e2e/vue-start/server-functions/package.json index 33384c9e564..27faff7ff22 100644 --- a/e2e/vue-start/server-functions/package.json +++ b/e2e/vue-start/server-functions/package.json @@ -32,7 +32,7 @@ "@types/node": "^22.10.2", "combinate": "^1.1.11", "postcss": "^8.5.1", - "srvx": "^0.8.6", + "srvx": "^0.9.8", "tailwindcss": "^4.1.17", "typescript": "^5.7.2", "@vitejs/plugin-vue": "^6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74a5cb37b6c..cc3db87b45b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5029,8 +5029,8 @@ importers: specifier: ^8.5.1 version: 8.5.6 srvx: - specifier: ^0.8.6 - version: 0.8.15 + specifier: ^0.9.8 + version: 0.9.8 tailwindcss: specifier: ^4.1.17 version: 4.1.17 From 8a769f9f54c69db4f937b24ca865515841c0cfcf Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:28:26 +0100 Subject: [PATCH 05/12] update srvx --- e2e/vue-start/server-routes/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/vue-start/server-routes/package.json b/e2e/vue-start/server-routes/package.json index 31a1444bccb..680edbd581b 100644 --- a/e2e/vue-start/server-routes/package.json +++ b/e2e/vue-start/server-routes/package.json @@ -32,7 +32,7 @@ "@types/node": "^22.10.2", "combinate": "^1.1.11", "postcss": "^8.5.1", - "srvx": "^0.8.6", + "srvx": "^0.9.8", "tailwindcss": "^4.1.17", "typescript": "^5.7.2", "@vitejs/plugin-vue": "^6.0.3", From a5a82e67e5ca8f5dc76c302d6637f69bff9616c4 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:29:06 +0100 Subject: [PATCH 06/12] update lockfile --- pnpm-lock.yaml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc3db87b45b..ed8f7c0744f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5105,8 +5105,8 @@ importers: specifier: ^8.5.1 version: 8.5.6 srvx: - specifier: ^0.8.6 - version: 0.8.15 + specifier: ^0.9.8 + version: 0.9.8 tailwindcss: specifier: ^4.1.17 version: 4.1.17 @@ -10006,7 +10006,7 @@ importers: devDependencies: '@netlify/vite-plugin-tanstack-start': specifier: ^1.1.4 - version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) '@tailwindcss/postcss': specifier: ^4.1.15 version: 4.1.15 @@ -26267,13 +26267,13 @@ snapshots: uuid: 11.1.0 write-file-atomic: 5.0.1 - '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)': + '@netlify/dev@4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/config': 23.2.0 '@netlify/dev-utils': 4.3.0 '@netlify/edge-functions-dev': 1.0.0 - '@netlify/functions-dev': 1.0.0(rollup@4.52.5) + '@netlify/functions-dev': 1.0.0(encoding@0.1.13)(rollup@4.52.5) '@netlify/headers': 2.1.0 '@netlify/images': 1.3.0(@netlify/blobs@10.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0) '@netlify/redirects': 3.1.0 @@ -26341,12 +26341,12 @@ snapshots: dependencies: '@netlify/types': 2.1.0 - '@netlify/functions-dev@1.0.0(rollup@4.52.5)': + '@netlify/functions-dev@1.0.0(encoding@0.1.13)(rollup@4.52.5)': dependencies: '@netlify/blobs': 10.1.0 '@netlify/dev-utils': 4.3.0 '@netlify/functions': 5.0.0 - '@netlify/zip-it-and-ship-it': 14.1.11(rollup@4.52.5) + '@netlify/zip-it-and-ship-it': 14.1.11(encoding@0.1.13)(rollup@4.52.5) cron-parser: 4.9.0 decache: 4.6.2 extract-zip: 2.0.1 @@ -26436,9 +26436,9 @@ snapshots: '@netlify/types@2.1.0': {} - '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin-tanstack-start@1.1.4(@tanstack/solid-start@packages+solid-start)(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + '@netlify/vite-plugin': 2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) optionalDependencies: '@tanstack/solid-start': link:packages/solid-start @@ -26466,9 +26466,9 @@ snapshots: - supports-color - uploadthing - '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': + '@netlify/vite-plugin@2.7.4(babel-plugin-macros@3.1.0)(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: - '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(ioredis@5.8.0)(rollup@4.52.5) + '@netlify/dev': 4.6.3(db0@0.3.4(@electric-sql/pglite@0.3.2)(@libsql/client@0.15.15)(mysql2@3.15.3))(encoding@0.1.13)(ioredis@5.8.0)(rollup@4.52.5) '@netlify/dev-utils': 4.3.0 dedent: 1.7.0(babel-plugin-macros@3.1.0) vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -26496,13 +26496,13 @@ snapshots: - supports-color - uploadthing - '@netlify/zip-it-and-ship-it@14.1.11(rollup@4.52.5)': + '@netlify/zip-it-and-ship-it@14.1.11(encoding@0.1.13)(rollup@4.52.5)': dependencies: '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@netlify/binary-info': 1.0.0 '@netlify/serverless-functions-api': 2.7.1 - '@vercel/nft': 0.29.4(rollup@4.52.5) + '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.52.5) archiver: 7.0.1 common-path-prefix: 3.0.0 copy-file: 11.1.0 @@ -29578,7 +29578,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/nft@0.29.4(rollup@4.52.5)': + '@vercel/nft@0.29.4(encoding@0.1.13)(rollup@4.52.5)': dependencies: '@mapbox/node-pre-gyp': 2.0.0(encoding@0.1.13) '@rollup/pluginutils': 5.1.4(rollup@4.52.5) From 8c0fbd1c6e6279c52b9aeecc3ef4b95d1dbfef70 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:30:41 +0100 Subject: [PATCH 07/12] define route as vue comp --- .../src/routes/factory/index.tsx | 119 ++++++++++-------- packages/vue-router/src/link.tsx | 27 ++-- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/e2e/vue-start/server-functions/src/routes/factory/index.tsx b/e2e/vue-start/server-functions/src/routes/factory/index.tsx index eddb385a673..8ddf7e6fbcf 100644 --- a/e2e/vue-start/server-functions/src/routes/factory/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/factory/index.tsx @@ -13,6 +13,7 @@ import { localFnPOST, } from './-functions/functions' import { computed, defineComponent, ref } from 'vue' +import type { PropType } from 'vue' const fnInsideRoute = createServerFn({ method: 'GET' }).handler(() => { return { @@ -132,58 +133,76 @@ interface TestCase { expected: any type: 'serverFn' | 'localFn' } -const Test = defineComponent((props: TestCase) => { - const result = ref(null) - const comparison = computed(() => { - if (result.value) { - const isEqual = deepEqual(result.value, props.expected) - return isEqual ? 'equal' : 'not equal' - } - return 'Loading...' - }) +const Test = defineComponent({ + props: { + fn: { + type: Function as PropType<() => Promise>, + required: true, + }, + expected: { + type: Object as PropType, + required: true, + }, + type: { + type: String as PropType, + required: true, + }, + }, + setup(props) { + const result = ref(null) + const comparison = computed(() => { + if (result.value) { + const isEqual = deepEqual(result.value, props.expected) + return isEqual ? 'equal' : 'not equal' + } + return 'Loading...' + }) - return () => ( -
-

-
- It should return{' '} - -
-            {props.type === 'serverFn' ? JSON.stringify(props.expected) : 'localFn'}
-          
-
-
-

- fn returns: -
- - {result.value - ? props.type === 'serverFn' - ? JSON.stringify(result.value) - : 'localFn' - : 'Loading...'} - {' '} - - {comparison.value} - -

- -
- ) +

+
+ It should return{' '} + +
+              {props.type === 'serverFn'
+                ? JSON.stringify(props.expected)
+                : 'localFn'}
+            
+
+
+

+ fn returns: +
+ + {result.value + ? props.type === 'serverFn' + ? JSON.stringify(result.value) + : 'localFn' + : 'Loading...'} + {' '} + + {comparison.value} + +

+ +
+ ) + }, }) const RouteComponent = defineComponent({ diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 806fee28d52..427c5b3abbb 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -591,8 +591,10 @@ export type LinkProps< export interface LinkPropsChildren { // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns children?: - | Vue.VNode - | ((state: { isActive: boolean; isTransitioning: boolean }) => Vue.VNode) + | Vue.VNodeChild + | (( + state: { isActive: boolean; isTransitioning: boolean }, + ) => Vue.VNodeChild) } type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap @@ -620,9 +622,12 @@ export type CreateLinkProps = LinkProps< string > -export type LinkComponent = < +export type LinkComponent< + in out TComp, + in out TDefaultFrom extends string = string, +> = < TRouter extends AnyRouter = RegisteredRouter, - const TFrom extends string = string, + const TFrom extends string = TDefaultFrom, const TTo extends string | undefined = undefined, const TMaskFrom extends string = TFrom, const TMaskTo extends string = '', @@ -657,7 +662,7 @@ export function createLink( name: 'CreatedLink', inheritAttrs: false, setup(_, { attrs, slots }) { - return () => Vue.h(Link, { ...attrs, _asChild: Comp }, slots) + return () => Vue.h(LinkImpl as any, { ...attrs, _asChild: Comp }, slots) }, }) as any } @@ -743,17 +748,7 @@ const LinkImpl = Vue.defineComponent({ /** * Link component with proper TypeScript generics support */ -export const Link = LinkImpl as unknown as { - < - TRouter extends AnyRouter = RegisteredRouter, - TFrom extends RoutePaths | string = string, - TTo extends string | undefined = '.', - TMaskFrom extends RoutePaths | string = TFrom, - TMaskTo extends string = '.', - >( - props: LinkComponentProps<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo>, - ): Vue.VNode -} +export const Link: LinkComponent<'a'> = LinkImpl as any function isCtrlEvent(e: MouseEvent) { return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) From f6639693048ce3c68a721966ab1f9ac2d489ccde Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:32:34 +0000 Subject: [PATCH 08/12] ci: apply automated fixes --- .../server-functions/src/routes/dead-code-preserve.tsx | 8 ++++++-- e2e/vue-start/server-functions/src/routes/env-only.tsx | 6 +++--- .../src/routes/formdata-redirect/target.$name.tsx | 5 ++++- .../server-functions/src/routes/primitives/index.tsx | 4 +--- .../server-functions/src/routes/raw-response.tsx | 4 +++- .../server-functions/src/routes/serialize-form-data.tsx | 5 +++-- e2e/vue-start/server-functions/vite.config.ts | 2 +- packages/vue-router/src/link.tsx | 7 ++++--- 8 files changed, 25 insertions(+), 16 deletions(-) diff --git a/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx index aa06aca3036..b80c6bf7701 100644 --- a/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx +++ b/e2e/vue-start/server-functions/src/routes/dead-code-preserve.tsx @@ -37,7 +37,9 @@ const RouteComponent = defineComponent({ return () => (

Dead code test

-

This server function writes to a file as a side effect, then reads it.

+

+ This server function writes to a file as a side effect, then reads it. +

) }, diff --git a/e2e/vue-start/server-functions/src/routes/env-only.tsx b/e2e/vue-start/server-functions/src/routes/env-only.tsx index 49bb03da8c3..6b784089c79 100644 --- a/e2e/vue-start/server-functions/src/routes/env-only.tsx +++ b/e2e/vue-start/server-functions/src/routes/env-only.tsx @@ -72,9 +72,9 @@ const RouteComponent = defineComponent({ {results.value.clientOnServer} When we called the function on the client: -
-                {results.value.clientOnClient}
-              
+
+              {results.value.clientOnClient}
+            
)}
diff --git a/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx index 7c9f682d165..958aeaf6177 100644 --- a/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx +++ b/e2e/vue-start/server-functions/src/routes/formdata-redirect/target.$name.tsx @@ -9,7 +9,10 @@ function RouteComponent() { return (
Hello{' '} - {params.value.name}! + + {params.value.name} + + !
) } diff --git a/e2e/vue-start/server-functions/src/routes/primitives/index.tsx b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx index 273ceff93a9..561f7646eb4 100644 --- a/e2e/vue-start/server-functions/src/routes/primitives/index.tsx +++ b/e2e/vue-start/server-functions/src/routes/primitives/index.tsx @@ -125,9 +125,7 @@ const RouteComponent = defineComponent({

result

- {query.isSuccess.value - ? stringify(query.data.value) - : ''} + {query.isSuccess.value ? stringify(query.data.value) : ''}

diff --git a/e2e/vue-start/server-functions/src/routes/raw-response.tsx b/e2e/vue-start/server-functions/src/routes/raw-response.tsx index e01b56e70d8..7c3b87d8633 100644 --- a/e2e/vue-start/server-functions/src/routes/raw-response.tsx +++ b/e2e/vue-start/server-functions/src/routes/raw-response.tsx @@ -36,7 +36,9 @@ const RouteComponent = defineComponent({
-
{JSON.stringify(formDataResult.value)}
+
+            {JSON.stringify(formDataResult.value)}
+          
) diff --git a/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx index cc0af7012c2..e2b117a9ad1 100644 --- a/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx +++ b/e2e/vue-start/server-functions/src/routes/serialize-form-data.tsx @@ -44,8 +44,9 @@ export const SerializeFormDataFnCall = defineComponent({ It should return{' '}
-              Hello, {testValues.name}! You are {testValues.age + testValues.__adder}{' '}
-              years old, and your favorite pets are {testValues.pet1},{testValues.pet2}.
+              Hello, {testValues.name}! You are{' '}
+              {testValues.age + testValues.__adder} years old, and your favorite
+              pets are {testValues.pet1},{testValues.pet2}.
             
diff --git a/e2e/vue-start/server-functions/vite.config.ts b/e2e/vue-start/server-functions/vite.config.ts index cf6484d5bc2..4c4af226a34 100644 --- a/e2e/vue-start/server-functions/vite.config.ts +++ b/e2e/vue-start/server-functions/vite.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ }, }, }), - vue(), + vue(), vueJsx(), ], }) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 427c5b3abbb..8ed5d9fe68a 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -592,9 +592,10 @@ export interface LinkPropsChildren { // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns children?: | Vue.VNodeChild - | (( - state: { isActive: boolean; isTransitioning: boolean }, - ) => Vue.VNodeChild) + | ((state: { + isActive: boolean + isTransitioning: boolean + }) => Vue.VNodeChild) } type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap From 89e6f6d38b44c136746c21e4746b81be3f4174ac Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 21:46:43 +0100 Subject: [PATCH 09/12] narrow type on link --- packages/vue-router/src/link.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 8ed5d9fe68a..87d555458ba 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -749,7 +749,8 @@ const LinkImpl = Vue.defineComponent({ /** * Link component with proper TypeScript generics support */ -export const Link: LinkComponent<'a'> = LinkImpl as any +export const Link = LinkImpl as unknown as Vue.Component & + LinkComponent<'a'> function isCtrlEvent(e: MouseEvent) { return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) From b97dd07ef93085fbf93d4a1e145f756a61fc9ff1 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 22:01:31 +0100 Subject: [PATCH 10/12] use @vue/runtime-dom --- packages/vue-router/package.json | 1 + packages/vue-router/src/link.tsx | 88 +++++++++++++++++--------------- pnpm-lock.yaml | 70 ++++++++++++------------- 3 files changed, 80 insertions(+), 79 deletions(-) diff --git a/packages/vue-router/package.json b/packages/vue-router/package.json index b708dafc044..f6b5df788ad 100644 --- a/packages/vue-router/package.json +++ b/packages/vue-router/package.json @@ -72,6 +72,7 @@ "@tanstack/history": "workspace:*", "@tanstack/router-core": "workspace:*", "@tanstack/vue-store": "^0.8.0", + "@vue/runtime-dom": "^3.5.25", "isbot": "^5.1.22", "jsesc": "^3.0.2", "tiny-invariant": "^1.3.3", diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 87d555458ba..f607455ed82 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -19,37 +19,37 @@ import type { RegisteredRouter, RoutePaths, } from '@tanstack/router-core' +import type { AnchorHTMLAttributes, ReservedProps } from '@vue/runtime-dom' import type { ValidateLinkOptions, ValidateLinkOptionsArray, } from './typePrimitives' -// Type definitions to replace missing Vue JSX types type EventHandler = (e: TEvent) => void -interface HTMLAttributes { - class?: string - style?: Record - onClick?: EventHandler - onFocus?: EventHandler - // Vue 3's h() function expects lowercase event names after 'on' prefix - onMouseenter?: EventHandler - onMouseleave?: EventHandler - onMouseover?: EventHandler - onMouseout?: EventHandler - onTouchstart?: EventHandler - // Also accept the camelCase versions for external API compatibility - onMouseEnter?: EventHandler - onMouseLeave?: EventHandler - onMouseOver?: EventHandler - onMouseOut?: EventHandler - onTouchStart?: EventHandler - [key: string]: any + +type DataAttributes = { + [K in `data-${string}`]?: unknown } +type LinkHTMLAttributes = AnchorHTMLAttributes & + ReservedProps & + DataAttributes & { + // Vue's runtime-dom types use lowercase event names. + // Also accept camelCase versions for external API compatibility. + onMouseEnter?: EventHandler + onMouseLeave?: EventHandler + onMouseOver?: EventHandler + onMouseOut?: EventHandler + onTouchStart?: EventHandler + + // `disabled` is not a valid attribute, but is useful when using `asChild`. + disabled?: boolean + } + interface StyledProps { - class?: string - style?: Record - [key: string]: any + class?: LinkHTMLAttributes['class'] + style?: LinkHTMLAttributes['style'] + [key: string]: unknown } export function useLinkProps< @@ -60,7 +60,7 @@ export function useLinkProps< TMaskTo extends string = '', >( options: UseLinkPropsOptions, -): HTMLAttributes { +): LinkHTMLAttributes { const router = useRouter() const isTransitioning = Vue.ref(false) let hasRenderFetched = false @@ -98,7 +98,7 @@ export function useLinkProps< const next = Vue.computed(() => { // Depend on search to rebuild when search changes currentSearch.value - return router.buildLocation(_options.value as any) + return router.buildLocation(_options.value) }) const preload = Vue.computed(() => { @@ -160,7 +160,7 @@ export function useLinkProps< }) const doPreload = () => - router.preloadRoute(_options.value as any).catch((err: any) => { + router.preloadRoute(_options.value).catch((err: any) => { console.warn(err) console.warn(preloadWarning) }) @@ -195,6 +195,7 @@ export function useLinkProps< // Create safe props that can be spread const getPropsSafeToSpread = () => { const result: Record = {} + const optionRecord = options as unknown as Record for (const key in options) { if ( ![ @@ -233,7 +234,7 @@ export function useLinkProps< 'additionalProps', ].includes(key) ) { - result[key] = options[key] + result[key] = optionRecord[key] } } return result @@ -241,7 +242,7 @@ export function useLinkProps< if (type.value === 'external') { // External links just have simple props - const externalProps: HTMLAttributes = { + const externalProps: Record = { ...getPropsSafeToSpread(), ref, href: options.to, @@ -265,11 +266,11 @@ export function useLinkProps< } }) - return externalProps + return externalProps as LinkHTMLAttributes } // The click handler - const handleClick = (e: MouseEvent): void => { + const handleClick = (e: PointerEvent): void => { // Check actual element's target attribute as fallback const elementTarget = ( e.currentTarget as HTMLAnchorElement | SVGAElement @@ -307,7 +308,7 @@ export function useLinkProps< startTransition: options.startTransition, viewTransition: options.viewTransition, ignoreBlocker: options.ignoreBlocker, - } as any) + }) } } @@ -448,7 +449,7 @@ export function useLinkProps< // Create static event handlers that don't change between renders const staticEventHandlers = { - onClick: composeEventHandlers([ + onClick: composeEventHandlers([ options.onClick, handleClick, ]) as any, @@ -480,8 +481,8 @@ export function useLinkProps< // Compute all props synchronously to avoid hydration mismatches // Using Vue.computed ensures props are calculated at render time, not after - const computedProps = Vue.computed(() => { - const result: HTMLAttributes = { + const computedProps = Vue.computed(() => { + const result: Record = { ...getPropsSafeToSpread(), href: href.value, ref, @@ -523,20 +524,20 @@ export function useLinkProps< for (const key of Object.keys(activeP)) { if (key !== 'class' && key !== 'style') { - result[key] = activeP[key] + result[key] = (activeP as any)[key] } } for (const key of Object.keys(inactiveP)) { if (key !== 'class' && key !== 'style') { - result[key] = inactiveP[key] + result[key] = (inactiveP as any)[key] } } - return result + return result as LinkHTMLAttributes }) // Return the computed ref itself - callers should access .value - return computedProps as unknown as HTMLAttributes + return computedProps as unknown as LinkHTMLAttributes } // Type definitions @@ -547,7 +548,7 @@ export type UseLinkPropsOptions< TMaskFrom extends RoutePaths | string = TFrom, TMaskTo extends string = '.', > = ActiveLinkOptions<'a', TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & - HTMLAttributes + LinkHTMLAttributes export type ActiveLinkOptions< TComp = 'a', @@ -560,7 +561,9 @@ export type ActiveLinkOptions< ActiveLinkOptionProps type ActiveLinkProps = Partial< - HTMLAttributes & { + (TComp extends keyof HTMLElementTagNameMap + ? LinkHTMLAttributes + : Record) & { [key: `data-${string}`]: unknown } > @@ -599,7 +602,7 @@ export interface LinkPropsChildren { } type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap - ? Omit + ? Omit : TComp extends Vue.Component ? Record : Record @@ -702,7 +705,7 @@ const LinkImpl = Vue.defineComponent({ const allProps = { ...props, ...attrs } const linkPropsComputed = useLinkProps( allProps as any, - ) as unknown as Vue.ComputedRef + ) as unknown as Vue.ComputedRef return () => { const Component = props._asChild || 'a' @@ -726,7 +729,7 @@ const LinkImpl = Vue.defineComponent({ if (Component === 'svg') { // Create props without class for svg link const svgLinkProps = { ...linkProps } - delete (svgLinkProps as any).class + delete (svgLinkProps).class return Vue.h('svg', {}, [Vue.h('a', svgLinkProps, slotContent)]) } @@ -750,6 +753,7 @@ const LinkImpl = Vue.defineComponent({ * Link component with proper TypeScript generics support */ export const Link = LinkImpl as unknown as Vue.Component & + Vue.Component & LinkComponent<'a'> function isCtrlEvent(e: MouseEvent) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ed8f7c0744f..bfee5c21683 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1458,7 +1458,7 @@ importers: version: 2.6.0 vite: specifier: ^7.1.7 - version: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) devDependencies: '@playwright/test': specifier: ^1.56.1 @@ -1477,7 +1477,7 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.6) @@ -1492,7 +1492,7 @@ importers: version: 5.9.2 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) e2e/react-start/custom-basepath: dependencies: @@ -7599,7 +7599,7 @@ importers: version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.56.1)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -10244,7 +10244,7 @@ importers: version: 2.11.10(@testing-library/jest-dom@6.6.3)(solid-js@1.9.10)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.56.1)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) web-vitals: specifier: ^5.1.0 version: 5.1.0 @@ -10735,16 +10735,16 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.3 - version: 5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) + version: 5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) '@vitejs/plugin-vue-jsx': specifier: ^4.1.2 - version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) + version: 4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2)) typescript: specifier: ^5.7.2 version: 5.9.2 vite: specifier: ^7.1.7 - version: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + version: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) vue-tsc: specifier: ^3.1.5 version: 3.1.5(typescript@5.9.2) @@ -11833,6 +11833,9 @@ importers: '@tanstack/vue-store': specifier: ^0.8.0 version: 0.8.0(vue@3.5.25(typescript@5.9.2)) + '@vue/runtime-dom': + specifier: ^3.5.25 + version: 3.5.25 isbot: specifier: ^5.1.22 version: 5.1.28 @@ -29639,18 +29642,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.7.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -29687,6 +29678,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-vue-jsx@4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + '@rolldown/pluginutils': 1.0.0-beta.40 + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) + vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vue: 3.5.25(typescript@5.9.2) + transitivePeerDependencies: + - supports-color + '@vitejs/plugin-vue-jsx@4.2.0(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.8.3))': dependencies: '@babel/core': 7.28.5 @@ -29721,6 +29723,11 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-vue@5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.9.2))': + dependencies: + vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) + vue: 3.5.25(typescript@5.9.2) + '@vitejs/plugin-vue@5.2.4(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vue@3.5.25(typescript@5.8.3))': dependencies: vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) @@ -29984,7 +29991,7 @@ snapshots: '@volar/language-core': 2.4.11 '@vue/compiler-dom': 3.5.14 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.14 + '@vue/shared': 3.5.25 computeds: 0.0.1 minimatch: 9.0.5 muggle-string: 0.4.1 @@ -29997,7 +30004,7 @@ snapshots: '@volar/language-core': 2.4.11 '@vue/compiler-dom': 3.5.14 '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.14 + '@vue/shared': 3.5.25 computeds: 0.0.1 minimatch: 9.0.5 muggle-string: 0.4.1 @@ -36553,17 +36560,6 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): - dependencies: - debug: 4.4.3 - globrex: 0.1.2 - tsconfck: 3.1.4(typescript@5.9.2) - optionalDependencies: - vite: 7.1.7(@types/node@22.10.2)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) - transitivePeerDependencies: - - supports-color - - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1)): dependencies: debug: 4.4.3 @@ -36613,7 +36609,7 @@ snapshots: optionalDependencies: vite: 7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1) - vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6(@types/node@22.10.2)(playwright@1.56.1)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4))(@vitest/ui@3.0.6(vitest@3.2.4))(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -36642,7 +36638,7 @@ snapshots: '@types/node': 22.10.2 '@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.56.1)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.0.6(vitest@3.2.4) - jsdom: 25.0.1 + jsdom: 27.0.0(postcss@8.5.6) transitivePeerDependencies: - jiti - less @@ -36657,7 +36653,7 @@ snapshots: - tsx - yaml - vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): + vitest@3.2.4(@types/node@22.10.2)(@vitest/browser@3.0.6)(@vitest/ui@3.0.6)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@22.10.2)(typescript@5.9.2))(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 @@ -36686,7 +36682,7 @@ snapshots: '@types/node': 22.10.2 '@vitest/browser': 3.0.6(@types/node@22.10.2)(playwright@1.56.1)(typescript@5.9.2)(vite@7.1.7(@types/node@22.10.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.37.0)(tsx@4.20.3)(yaml@2.8.1))(vitest@3.2.4) '@vitest/ui': 3.0.6(vitest@3.2.4) - jsdom: 27.0.0(postcss@8.5.6) + jsdom: 25.0.1 transitivePeerDependencies: - jiti - less From b2f8270b2b66c3434a7f6c2c84b9626149fa8062 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Wed, 17 Dec 2025 22:06:50 +0100 Subject: [PATCH 11/12] fix eslint --- packages/vue-router/src/link.tsx | 19 +++++++++++++------ .../src/ssr/renderRouterToStream.tsx | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index f607455ed82..5818878f9f2 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -52,6 +52,15 @@ interface StyledProps { [key: string]: unknown } +type PropsOfComponent = + // Functional components + TComp extends (props: infer P, ...args: Array) => any + ? P + : // Vue components (defineComponent, class components, etc) + TComp extends Vue.Component + ? P + : Record + export function useLinkProps< TRouter extends AnyRouter = RegisteredRouter, TFrom extends RoutePaths | string = string, @@ -98,7 +107,7 @@ export function useLinkProps< const next = Vue.computed(() => { // Depend on search to rebuild when search changes currentSearch.value - return router.buildLocation(_options.value) + return router.buildLocation(_options.value as any) }) const preload = Vue.computed(() => { @@ -160,7 +169,7 @@ export function useLinkProps< }) const doPreload = () => - router.preloadRoute(_options.value).catch((err: any) => { + router.preloadRoute(_options.value as any).catch((err: any) => { console.warn(err) console.warn(preloadWarning) }) @@ -563,7 +572,7 @@ export type ActiveLinkOptions< type ActiveLinkProps = Partial< (TComp extends keyof HTMLElementTagNameMap ? LinkHTMLAttributes - : Record) & { + : PropsOfComponent) & { [key: `data-${string}`]: unknown } > @@ -603,9 +612,7 @@ export interface LinkPropsChildren { type LinkComponentVueProps = TComp extends keyof HTMLElementTagNameMap ? Omit - : TComp extends Vue.Component - ? Record - : Record + : Omit, keyof CreateLinkProps> export type LinkComponentProps< TComp = 'a', diff --git a/packages/vue-router/src/ssr/renderRouterToStream.tsx b/packages/vue-router/src/ssr/renderRouterToStream.tsx index ed2a5e6de32..d8649ff1bdf 100644 --- a/packages/vue-router/src/ssr/renderRouterToStream.tsx +++ b/packages/vue-router/src/ssr/renderRouterToStream.tsx @@ -14,7 +14,7 @@ function prependDoctype( let sentDoctype = false return new NodeReadableStream({ - async start(controller) { + start(controller) { const reader = readable.getReader() async function pump(): Promise { From 5af2ea1ef04fb1049900351ee208221f92724bb2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 17 Dec 2025 21:11:58 +0000 Subject: [PATCH 12/12] ci: apply automated fixes --- packages/vue-router/src/link.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vue-router/src/link.tsx b/packages/vue-router/src/link.tsx index 5818878f9f2..998aecef2ad 100644 --- a/packages/vue-router/src/link.tsx +++ b/packages/vue-router/src/link.tsx @@ -736,7 +736,7 @@ const LinkImpl = Vue.defineComponent({ if (Component === 'svg') { // Create props without class for svg link const svgLinkProps = { ...linkProps } - delete (svgLinkProps).class + delete svgLinkProps.class return Vue.h('svg', {}, [Vue.h('a', svgLinkProps, slotContent)]) }