From 0fa55912c52aaf5891052f5b43b7bcc76980f671 Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 10:05:25 +0100 Subject: [PATCH 001/100] adding react things --- package.json | 2 +- pnpm-lock.yaml | 357 +++++++++++++++++- spiceflow/package.json | 2 + spiceflow/src/react/entry.client.tsx | 120 ++++++ spiceflow/src/react/entry.rsc.tsx | 101 +++++ spiceflow/src/react/entry.ssr.tsx | 72 ++++ spiceflow/src/react/types/ambient.d.ts | 61 +++ spiceflow/src/react/types/index.ts | 19 + spiceflow/src/react/utils/client-reference.ts | 25 ++ spiceflow/src/react/utils/fetch.ts | 77 ++++ spiceflow/src/react/utils/stream-script.ts | 45 +++ spiceflow/src/vite.tsx | 316 ++++++++++++++++ website/package.json | 2 +- 13 files changed, 1188 insertions(+), 11 deletions(-) create mode 100644 spiceflow/src/react/entry.client.tsx create mode 100644 spiceflow/src/react/entry.rsc.tsx create mode 100644 spiceflow/src/react/entry.ssr.tsx create mode 100644 spiceflow/src/react/types/ambient.d.ts create mode 100644 spiceflow/src/react/types/index.ts create mode 100644 spiceflow/src/react/utils/client-reference.ts create mode 100644 spiceflow/src/react/utils/fetch.ts create mode 100644 spiceflow/src/react/utils/stream-script.ts create mode 100644 spiceflow/src/vite.tsx diff --git a/package.json b/package.json index ec78b95..b1bc158 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "spiceflow": "workspace:*", "tsx": "^4.19.2", "typescript": "^5.7.3", - "vite": "^6.0.11", + "vite": "^6.1.0", "vitest": "^3.0.4" }, "repository": "https://github.com/remorses/", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a9753b..9a89a7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,8 +31,8 @@ importers: specifier: ^5.7.3 version: 5.7.3 vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + specifier: ^6.1.0 + version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vitest: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) @@ -146,6 +146,9 @@ importers: '@sinclair/typebox': specifier: ^0.34.14 version: 0.34.15 + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -161,6 +164,9 @@ importers: openapi-types: specifier: ^12.1.3 version: 12.1.3 + react-server-dom-vite: + specifier: npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14 + version: '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0)' superjson: specifier: ^2.2.2 version: 2.2.2 @@ -275,8 +281,8 @@ importers: specifier: ^5.7.3 version: 5.7.3 vite: - specifier: ^6.0.11 - version: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + specifier: ^6.1.0 + version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vite-tsconfig-paths: specifier: ^4.2.1 version: 4.3.2(typescript@5.7.3)(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) @@ -459,6 +465,18 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: {integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: {integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.26.7': resolution: {integrity: sha512-5cJurntg+AT+cgelGP9Bt788DKiAw9gIMSMU2NJrLAilnj0m8WZWUNZPSLOmadYsujHutpgElO+50foX+ib/Wg==} engines: {node: '>=6.9.0'} @@ -1389,6 +1407,13 @@ packages: resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} engines: {node: '>=8'} + '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14': + resolution: {integrity: sha512-4bBG0uLS/XuddOi1KjJb6j/A49rdEe62yzcFjd7jghQmPXT8jNinyIislylz9xupJB3TvHB8JGv5/PB+LPASHg==} + engines: {node: '>=0.10.0'} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1593,96 +1618,191 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.34.6': + resolution: {integrity: sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.34.0': resolution: {integrity: sha512-yVh0Kf1f0Fq4tWNf6mWcbQBCLDpDrDEl88lzPgKhrgTcDrTtlmun92ywEF9dCjmYO3EFiSuJeeo9cYRxl2FswA==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.34.6': + resolution: {integrity: sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.34.0': resolution: {integrity: sha512-gCs0ErAZ9s0Osejpc3qahTsqIPUDjSKIyxK/0BGKvL+Tn0n3Kwvj8BrCv7Y5sR1Ypz1K2qz9Ny0VvkVyoXBVUQ==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.34.6': + resolution: {integrity: sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.34.0': resolution: {integrity: sha512-aIB5Anc8hngk15t3GUkiO4pv42ykXHfmpXGS+CzM9CTyiWyT8HIS5ygRAy7KcFb/wiw4Br+vh1byqcHRTfq2tQ==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.34.6': + resolution: {integrity: sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.34.0': resolution: {integrity: sha512-kpdsUdMlVJMRMaOf/tIvxk8TQdzHhY47imwmASOuMajg/GXpw8GKNd8LNwIHE5Yd1onehNpcUB9jHY6wgw9nHQ==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.34.6': + resolution: {integrity: sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.34.0': resolution: {integrity: sha512-D0RDyHygOBCQiqookcPevrvgEarN0CttBecG4chOeIYCNtlKHmf5oi5kAVpXV7qs0Xh/WO2RnxeicZPtT50V0g==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.34.6': + resolution: {integrity: sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.34.0': resolution: {integrity: sha512-mCIw8j5LPDXmCOW8mfMZwT6F/Kza03EnSr4wGYEswrEfjTfVsFOxvgYfuRMxTuUF/XmRb9WSMD5GhCWDe2iNrg==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.34.0': resolution: {integrity: sha512-AwwldAu4aCJPob7zmjuDUMvvuatgs8B/QiVB0KwkUarAcPB3W+ToOT+18TQwY4z09Al7G0BvCcmLRop5zBLTag==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.34.0': resolution: {integrity: sha512-e7kDUGVP+xw05pV65ZKb0zulRploU3gTu6qH1qL58PrULDGxULIS0OSDQJLH7WiFnpd3ZKUU4VM3u/Z7Zw+e7Q==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.34.6': + resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.34.0': resolution: {integrity: sha512-SXYJw3zpwHgaBqTXeAZ31qfW/v50wq4HhNVvKFhRr5MnptRX2Af4KebLWR1wpxGJtLgfS2hEPuALRIY3LPAAcA==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.34.6': + resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.34.0': resolution: {integrity: sha512-e5XiCinINCI4RdyU3sFyBH4zzz7LiQRvHqDtRe9Dt8o/8hTBaYpdPimayF00eY2qy5j4PaaWK0azRgUench6WQ==} cpu: [loong64] os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.34.0': resolution: {integrity: sha512-3SWN3e0bAsm9ToprLFBSro8nJe6YN+5xmB11N4FfNf92wvLye/+Rh5JGQtKOpwLKt6e61R1RBc9g+luLJsc23A==} cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.34.0': resolution: {integrity: sha512-B1Oqt3GLh7qmhvfnc2WQla4NuHlcxAD5LyueUi5WtMc76ZWY+6qDtQYqnxARx9r+7mDGfamD+8kTJO0pKUJeJA==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.34.0': resolution: {integrity: sha512-UfUCo0h/uj48Jq2lnhX0AOhZPSTAq3Eostas+XZ+GGk22pI+Op1Y6cxQ1JkUuKYu2iU+mXj1QjPrZm9nNWV9rg==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.34.6': + resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.34.0': resolution: {integrity: sha512-chZLTUIPbgcpm+Z7ALmomXW8Zh+wE2icrG+K6nt/HenPLmtwCajhQC5flNSk1Xy5EDMt/QAOz2MhzfOfJOLSiA==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.34.6': + resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.34.0': resolution: {integrity: sha512-jo0UolK70O28BifvEsFD/8r25shFezl0aUk2t0VJzREWHkq19e+pcLu4kX5HiVXNz5qqkD+aAq04Ct8rkxgbyQ==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.34.6': + resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.34.0': resolution: {integrity: sha512-Vmg0NhAap2S54JojJchiu5An54qa6t/oKT7LmDaWggpIcaiL8WcWHEN6OQrfTdL6mQ2GFyH7j2T5/3YPEDOOGA==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.34.6': + resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.34.0': resolution: {integrity: sha512-CV2aqhDDOsABKHKhNcs1SZFryffQf8vK2XrxP6lxC99ELZAdvsDgPklIBfd65R8R+qvOm1SmLaZ/Fdq961+m7A==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.34.6': + resolution: {integrity: sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.34.0': resolution: {integrity: sha512-g2ASy1QwHP88y5KWvblUolJz9rN+i4ZOsYzkEwcNfaNooxNUXG+ON6F5xFo0NIItpHqxcdAyls05VXpBnludGw==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.34.6': + resolution: {integrity: sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==} + cpu: [x64] + os: [win32] + '@sinclair/typebox@0.34.15': resolution: {integrity: sha512-xeIzl3h1Znn9w/LTITqpiwag0gXjA+ldi2ZkXIBxGEppGCW211Tza+eL6D4pKqs10bj5z2umBWk5WL6spQ2OCQ==} @@ -1716,6 +1836,18 @@ packages: '@types/acorn@4.0.6': resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1858,6 +1990,12 @@ packages: '@vanilla-extract/private@1.0.6': resolution: {integrity: sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw==} + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitest/expect@3.0.4': resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} @@ -4578,6 +4716,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.34.6: + resolution: {integrity: sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5319,6 +5462,46 @@ packages: yaml: optional: true + vite@6.1.0: + resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@3.0.4: resolution: {integrity: sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -5714,6 +5897,16 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.7)': + dependencies: + '@babel/core': 7.26.7 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.7)': + dependencies: + '@babel/core': 7.26.7 + '@babel/helper-plugin-utils': 7.26.5 + '@babel/plugin-transform-typescript@7.26.7(@babel/core@7.26.7)': dependencies: '@babel/core': 7.26.7 @@ -6386,6 +6579,11 @@ snapshots: '@istanbuljs/schema@0.1.3': {} + '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -6747,60 +6945,117 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.34.0': optional: true + '@rollup/rollup-android-arm-eabi@4.34.6': + optional: true + '@rollup/rollup-android-arm64@4.34.0': optional: true + '@rollup/rollup-android-arm64@4.34.6': + optional: true + '@rollup/rollup-darwin-arm64@4.34.0': optional: true + '@rollup/rollup-darwin-arm64@4.34.6': + optional: true + '@rollup/rollup-darwin-x64@4.34.0': optional: true + '@rollup/rollup-darwin-x64@4.34.6': + optional: true + '@rollup/rollup-freebsd-arm64@4.34.0': optional: true + '@rollup/rollup-freebsd-arm64@4.34.6': + optional: true + '@rollup/rollup-freebsd-x64@4.34.0': optional: true + '@rollup/rollup-freebsd-x64@4.34.6': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.34.0': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.34.6': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.34.0': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.34.6': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.34.0': optional: true + '@rollup/rollup-linux-arm64-gnu@4.34.6': + optional: true + '@rollup/rollup-linux-arm64-musl@4.34.0': optional: true + '@rollup/rollup-linux-arm64-musl@4.34.6': + optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.34.0': optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.34.6': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.34.0': optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.34.0': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.34.6': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.34.0': optional: true + '@rollup/rollup-linux-s390x-gnu@4.34.6': + optional: true + '@rollup/rollup-linux-x64-gnu@4.34.0': optional: true + '@rollup/rollup-linux-x64-gnu@4.34.6': + optional: true + '@rollup/rollup-linux-x64-musl@4.34.0': optional: true + '@rollup/rollup-linux-x64-musl@4.34.6': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.34.0': optional: true + '@rollup/rollup-win32-arm64-msvc@4.34.6': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.34.0': optional: true + '@rollup/rollup-win32-ia32-msvc@4.34.6': + optional: true + '@rollup/rollup-win32-x64-msvc@4.34.0': optional: true + '@rollup/rollup-win32-x64-msvc@4.34.6': + optional: true + '@sinclair/typebox@0.34.15': {} '@stefanprobst/rehype-extract-toc@2.2.1': @@ -6843,6 +7098,27 @@ snapshots: dependencies: '@types/estree': 1.0.6 + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.7 + '@babel/types': 7.26.7 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.7 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.7 + '@babel/types': 7.26.7 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.7 + '@types/cookie@0.6.0': {} '@types/debug@4.1.12': @@ -7048,6 +7324,17 @@ snapshots: '@vanilla-extract/private@1.0.6': {} + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + dependencies: + '@babel/core': 7.26.7 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.7) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.7) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@3.0.4': dependencies: '@vitest/spy': 3.0.4 @@ -7055,13 +7342,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.4(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@vitest/mocker@3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) '@vitest/pretty-format@3.0.4': dependencies: @@ -10616,6 +10903,31 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.0 fsevents: 2.3.3 + rollup@4.34.6: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.34.6 + '@rollup/rollup-android-arm64': 4.34.6 + '@rollup/rollup-darwin-arm64': 4.34.6 + '@rollup/rollup-darwin-x64': 4.34.6 + '@rollup/rollup-freebsd-arm64': 4.34.6 + '@rollup/rollup-freebsd-x64': 4.34.6 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.6 + '@rollup/rollup-linux-arm-musleabihf': 4.34.6 + '@rollup/rollup-linux-arm64-gnu': 4.34.6 + '@rollup/rollup-linux-arm64-musl': 4.34.6 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.6 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.6 + '@rollup/rollup-linux-riscv64-gnu': 4.34.6 + '@rollup/rollup-linux-s390x-gnu': 4.34.6 + '@rollup/rollup-linux-x64-gnu': 4.34.6 + '@rollup/rollup-linux-x64-musl': 4.34.6 + '@rollup/rollup-win32-arm64-msvc': 4.34.6 + '@rollup/rollup-win32-ia32-msvc': 4.34.6 + '@rollup/rollup-win32-x64-msvc': 4.34.6 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -11427,7 +11739,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.2 - vite: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -11475,11 +11787,38 @@ snapshots: terser: 5.31.6 tsx: 4.19.2 yaml: 2.7.0 + optional: true + + vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.5.1 + rollup: 4.34.6 + optionalDependencies: + '@types/node': 22.12.0 + fsevents: 2.3.3 + jiti: 1.21.7 + terser: 5.31.6 + tsx: 4.19.2 + yaml: 2.7.0 + + vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.5.1 + rollup: 4.34.6 + optionalDependencies: + '@types/node': 22.13.0 + fsevents: 2.3.3 + jiti: 1.21.7 + terser: 5.31.6 + tsx: 4.19.2 + yaml: 2.7.0 vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.4 - '@vitest/mocker': 3.0.4(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/mocker': 3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.4 '@vitest/runner': 3.0.4 '@vitest/snapshot': 3.0.4 @@ -11495,7 +11834,7 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vite-node: 3.0.4(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: diff --git a/spiceflow/package.json b/spiceflow/package.json index 296a4fb..967c15f 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -55,6 +55,8 @@ "dependencies": { "@medley/router": "^0.2.1", "@sinclair/typebox": "^0.34.14", + "@vitejs/plugin-react": "^4.3.4", + "react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "eventsource-parser": "^3.0.0", diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx new file mode 100644 index 0000000..b4c1229 --- /dev/null +++ b/spiceflow/src/react/entry.client.tsx @@ -0,0 +1,120 @@ +import React from "react"; +import ReactDomClient from "react-dom/client"; +import ReactClient from "react-server-dom-vite/client"; +import type { ServerPayload } from "./entry.rsc"; +import type { CallServerFn } from "./types"; +import { clientReferenceManifest } from "./utils/client-reference"; +import { getFlightStreamBrowser } from "./utils/stream-script"; + +async function main() { + const callServer: CallServerFn = async (id, args) => { + const url = new URL(window.location.href); + url.searchParams.set("__rsc", id); + const payload = await ReactClient.createFromFetch( + fetch(url, { + method: "POST", + body: await ReactClient.encodeReply(args), + }), + clientReferenceManifest, + { callServer }, + ); + setPayload(payload); + return payload.returnValue; + }; + Object.assign(globalThis, { __callServer: callServer }); + + async function onNavigation() { + const url = new URL(window.location.href); + url.searchParams.set("__rsc", ""); + const payload = await ReactClient.createFromFetch( + fetch(url), + clientReferenceManifest, + { callServer }, + ); + setPayload(payload); + } + + const initialPayload = + await ReactClient.createFromReadableStream( + getFlightStreamBrowser(), + clientReferenceManifest, + { callServer }, + ); + + let setPayload: (v: ServerPayload) => void; + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload); + const [_isPending, startTransition] = React.useTransition(); + + React.useEffect(() => { + setPayload = (v) => startTransition(() => setPayload_(v)); + }, [startTransition, setPayload_]); + + React.useEffect(() => { + return listenNavigation(onNavigation); + }, []); + + return payload.root; + } + + ReactDomClient.hydrateRoot(document, , { + formState: initialPayload.formState, + }); + + if (import.meta.hot) { + import.meta.hot.on("react-server:update", (e) => { + console.log("[react-server:update]", e.file); + window.history.replaceState({}, "", window.location.href); + }); + } +} + +function listenNavigation(onNavigation: () => void) { + window.addEventListener("popstate", onNavigation); + + const oldPushState = window.history.pushState; + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args); + onNavigation(); + return res; + }; + + const oldReplaceState = window.history.replaceState; + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args); + onNavigation(); + return res; + }; + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest("a"); + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === "_self") && + link.origin === location.origin && + !link.hasAttribute("download") && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault(); + history.pushState(null, "", link.href); + } + } + document.addEventListener("click", onClick); + + return () => { + document.removeEventListener("click", onClick); + window.removeEventListener("popstate", onNavigation); + window.history.pushState = oldPushState; + window.history.replaceState = oldReplaceState; + }; +} + +main(); diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx new file mode 100644 index 0000000..b5fea1e --- /dev/null +++ b/spiceflow/src/react/entry.rsc.tsx @@ -0,0 +1,101 @@ +import type { ReactFormState } from "react-dom/client"; +import ReactServer from "react-server-dom-vite/server"; +import { Router } from "./app/routes"; +import type { + ClientReferenceMetadataManifest, + ServerReferenceManifest, +} from "./types"; +import { fromPipeableToWebReadable } from "./utils/fetch"; + +export interface RscHandlerResult { + stream: ReadableStream; +} + +export interface ServerPayload { + root: React.ReactNode; + formState?: ReactFormState; + returnValue?: unknown; +} + +export async function handler( + url: URL, + request: Request, +): Promise { + // handle action + let returnValue: unknown | undefined; + let formState: ReactFormState | undefined; + if (request.method === "POST") { + const actionId = url.searchParams.get("__rsc"); + if (actionId) { + // client stream request + const contentType = request.headers.get("content-type"); + const body = contentType?.startsWith("multipart/form-data") + ? await request.formData() + : await request.text(); + const args = await ReactServer.decodeReply(body); + const reference = + serverReferenceManifest.resolveServerReference(actionId); + await reference.preload(); + const action = await reference.get(); + returnValue = await (action as any).apply(null, args); + } else { + // progressive enhancement + const formData = await request.formData(); + const decodedAction = await ReactServer.decodeAction( + formData, + serverReferenceManifest, + ); + formState = await ReactServer.decodeFormState( + await decodedAction(), + formData, + serverReferenceManifest, + ); + } + } + + // render flight stream + const stream = fromPipeableToWebReadable( + ReactServer.renderToPipeableStream( + { + root: , + returnValue, + formState, + }, + clientReferenceMetadataManifest, + {}, + ), + ); + + return { + stream, + }; +} + +const serverReferenceManifest: ServerReferenceManifest = { + resolveServerReference(reference: string) { + const [id, name] = reference.split("#"); + let resolved: unknown; + return { + async preload() { + let mod: Record; + if (import.meta.env.DEV) { + mod = await import(/* @vite-ignore */ id); + } else { + const references = await import("virtual:build-server-references"); + mod = await references.default[id](); + } + resolved = mod[name]; + }, + get() { + return resolved; + }, + }; + }, +}; + +const clientReferenceMetadataManifest: ClientReferenceMetadataManifest = { + resolveClientReferenceMetadata(metadata) { + // console.log("[debug:resolveClientReferenceMetadata]", { metadata }, Object.getOwnPropertyDescriptors(metadata)); + return metadata.$$id; + }, +}; diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx new file mode 100644 index 0000000..1fc3726 --- /dev/null +++ b/spiceflow/src/react/entry.ssr.tsx @@ -0,0 +1,72 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import ReactDomServer from "react-dom/server"; +import ReactClient from "react-server-dom-vite/client"; +import type { ModuleRunner } from "vite/module-runner"; +import type { ServerPayload } from "./entry.rsc"; +import { clientReferenceManifest } from "./utils/client-reference"; +import { + createRequest, + fromPipeableToWebReadable, + fromWebToNodeReadable, + sendResponse, +} from "./utils/fetch"; +import { injectFlightStream } from "./utils/stream-script"; + +export default async function handler( + req: IncomingMessage, + res: ServerResponse, +) { + const request = createRequest(req, res); + const url = new URL(request.url); + const rscEntry = await importRscEntry(); + const rscResult = await rscEntry.handler(url, request); + + if (url.searchParams.has("__rsc")) { + const response = new Response(rscResult.stream, { + headers: { + "content-type": "text/x-component;charset=utf-8", + }, + }); + sendResponse(response, res); + return; + } + + const [flightStream1, flightStream2] = rscResult.stream.tee(); + + const payload = await ReactClient.createFromNodeStream( + fromWebToNodeReadable(flightStream1), + clientReferenceManifest, + ); + + const ssrAssets = await import("virtual:ssr-assets"); + + const htmlStream = fromPipeableToWebReadable( + ReactDomServer.renderToPipeableStream(payload.root, { + bootstrapModules: ssrAssets.bootstrapModules, + // @ts-ignore no type? + formState: payload.formState, + }), + ); + + const response = new Response( + htmlStream + .pipeThrough(new TextDecoderStream()) + .pipeThrough(injectFlightStream(flightStream2)), + { + headers: { + "content-type": "text/html;charset=utf-8", + }, + }, + ); + sendResponse(response, res); +} + +declare let __rscRunner: ModuleRunner; + +async function importRscEntry(): Promise { + if (import.meta.env.DEV) { + return await __rscRunner.import("/src/entry.rsc.tsx"); + } else { + return await import("virtual:build-rsc-entry" as any); + } +} diff --git a/spiceflow/src/react/types/ambient.d.ts b/spiceflow/src/react/types/ambient.d.ts new file mode 100644 index 0000000..2414f28 --- /dev/null +++ b/spiceflow/src/react/types/ambient.d.ts @@ -0,0 +1,61 @@ +/// + +declare module "react-server-dom-vite/server" { + export function renderToPipeableStream( + data: T, + manifest: import(".").ClientReferenceMetadataManifest, + opitons?: unknown, + ): import("react-dom/server").PipeableStream; + + export function decodeReply(body: string | FormData): Promise; + + export function decodeAction( + body: FormData, + manifest: import(".").ServerReferenceManifest, + ): Promise<() => Promise>; + + export function decodeFormState( + returnValue: unknown, + body: FormData, + manifest: import(".").ServerReferenceManifest, + ): Promise; +} + +declare module "react-server-dom-vite/client" { + export function createFromNodeStream( + stream: import("node:stream").Readable, + manifest: import(".").ClientReferenceManifest, + ): Promise; + + export function createFromReadableStream( + stream: ReadableStream, + manifest: import(".").ClientReferenceManifest, + options: { + callServer: import(".").CallServerFn; + }, + ): Promise; + + export function createFromFetch( + fetchReturn: ReturnType, + manifest: unknown, + options: { + callServer: import(".").CallServerFn; + }, + ): Promise; + + export function encodeReply(v: unknown[]): Promise; +} + +declare module "virtual:ssr-assets" { + export const bootstrapModules: string[]; +} + +declare module "virtual:build-client-references" { + const value: Record Promise>>; + export default value; +} + +declare module "virtual:build-server-references" { + const value: Record Promise>>; + export default value; +} diff --git a/spiceflow/src/react/types/index.ts b/spiceflow/src/react/types/index.ts new file mode 100644 index 0000000..59edbc4 --- /dev/null +++ b/spiceflow/src/react/types/index.ts @@ -0,0 +1,19 @@ +export type ClientReferenceMetadataManifest = { + resolveClientReferenceMetadata(metadata: { $$id: string }): string; +}; + +export type ClientReferenceManifest = { + resolveClientReference(reference: string): { + preload(): Promise; + get(): unknown; + }; +}; + +export type ServerReferenceManifest = { + resolveServerReference(reference: string): { + preload(): Promise; + get(): unknown; + }; +}; + +export type CallServerFn = (id: string, args: unknown[]) => unknown; diff --git a/spiceflow/src/react/utils/client-reference.ts b/spiceflow/src/react/utils/client-reference.ts new file mode 100644 index 0000000..efe4553 --- /dev/null +++ b/spiceflow/src/react/utils/client-reference.ts @@ -0,0 +1,25 @@ +import type { ClientReferenceManifest } from "../types"; + +export const clientReferenceManifest: ClientReferenceManifest = { + resolveClientReference(reference: string) { + const [id, name] = reference.split("#"); + let resolved: unknown; + return { + async preload() { + let mod: Record; + if (import.meta.env.DEV) { + mod = await import(/* @vite-ignore */ id); + } else { + const references = await import( + "virtual:build-client-references" as string + ); + mod = await references.default[id](); + } + resolved = mod[name]; + }, + get() { + return resolved; + }, + }; + }, +}; diff --git a/spiceflow/src/react/utils/fetch.ts b/spiceflow/src/react/utils/fetch.ts new file mode 100644 index 0000000..2d45071 --- /dev/null +++ b/spiceflow/src/react/utils/fetch.ts @@ -0,0 +1,77 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import { PassThrough, Readable } from "node:stream"; +import type { PipeableStream } from "react-dom/server"; + +export function createRequest( + req: IncomingMessage, + res: ServerResponse, +): Request { + const abortController = new AbortController(); + res.once("close", () => { + if (req.destroyed) { + abortController.abort(); + } + }); + + const headers = new Headers(); + for (const [k, v] of Object.entries(req.headers)) { + if (k.startsWith(":")) { + continue; + } + if (typeof v === "string") { + headers.set(k, v); + } else if (Array.isArray(v)) { + v.forEach((v) => headers.append(k, v)); + } + } + + return new Request( + new URL( + req.url || "/", + `${headers.get("x-forwarded-proto") ?? "http"}://${ + req.headers.host || "unknown.local" + }`, + ), + { + method: req.method, + body: + req.method === "GET" || req.method === "HEAD" + ? null + : (Readable.toWeb(req) as any), + headers, + signal: abortController.signal, + // @ts-ignore for undici + duplex: "half", + }, + ); +} + +export function sendResponse(response: Response, res: ServerResponse) { + const headers = Object.fromEntries(response.headers); + if (headers["set-cookie"]) { + delete headers["set-cookie"]; + res.setHeader("set-cookie", response.headers.getSetCookie()); + } + res.writeHead(response.status, response.statusText, headers); + + if (response.body) { + const abortController = new AbortController(); + res.once("close", () => abortController.abort()); + res.once("error", () => abortController.abort()); + Readable.fromWeb(response.body as any, { + signal: abortController.signal, + }).pipe(res); + } else { + res.end(); + } +} + +export function fromPipeableToWebReadable(stream: PipeableStream) { + return Readable.toWeb( + stream.pipe(new PassThrough()), + ) as ReadableStream; +} + +export function fromWebToNodeReadable(stream: ReadableStream) { + return Readable.fromWeb(stream as any); +} diff --git a/spiceflow/src/react/utils/stream-script.ts b/spiceflow/src/react/utils/stream-script.ts new file mode 100644 index 0000000..cedea64 --- /dev/null +++ b/spiceflow/src/react/utils/stream-script.ts @@ -0,0 +1,45 @@ +const INIT_SCRIPT = ` +self.__flightStream = new ReadableStream({ + start(controller) { + self.__f_push = (c) => controller.enqueue(c); + self.__f_close = () => controller.close(); + } +}).pipeThrough(new TextEncoderStream()); +`; + +export function injectFlightStream(stream: ReadableStream) { + return new TransformStream({ + async transform(chunk, controller) { + // TODO: chunk is not guaranteed to include entire end tag `` + if (chunk.includes("")) { + chunk = chunk.replace( + "", + () => ``, + ); + } + if (chunk.includes("")) { + const i = chunk.indexOf(""); + controller.enqueue(chunk.slice(0, i)); + await stream.pipeThrough(new TextDecoderStream()).pipeTo( + new WritableStream({ + write(chunk) { + controller.enqueue( + ``, + ); + }, + close() { + controller.enqueue(``); + }, + }), + ); + controller.enqueue(chunk.slice(i)); + } else { + controller.enqueue(chunk); + } + }, + }); +} + +export function getFlightStreamBrowser(): ReadableStream { + return (self as any).__flightStream; +} diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx new file mode 100644 index 0000000..e62b2ed --- /dev/null +++ b/spiceflow/src/vite.tsx @@ -0,0 +1,316 @@ +import assert from 'node:assert' +import path from 'node:path' +import react from '@vitejs/plugin-react' +import { + type Manifest, + type Plugin, + type RunnableDevEnvironment, + createRunnableDevEnvironment, + defineConfig, +} from 'vite' + +// state for build orchestration +let browserManifest: Manifest +let clientReferences: Record = {} // TODO: normalize id +let serverReferences: Record = {} +let buildScan = false + +export default defineConfig({ + appType: 'custom', + environments: { + client: { + optimizeDeps: { + include: ['react-dom/client', 'react-server-dom-vite/client'], + }, + build: { + manifest: true, + outDir: 'dist/client', + rollupOptions: { + input: { index: 'virtual:browser-entry' }, + }, + }, + }, + ssr: { + build: { + outDir: 'dist/ssr', + rollupOptions: { + input: { index: '/src/entry.ssr.tsx' }, + }, + }, + }, + rsc: { + optimizeDeps: { + include: [ + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-server-dom-vite/server', + ], + }, + resolve: { + conditions: ['react-server'], + noExternal: true, + }, + dev: { + createEnvironment(name, config) { + return createRunnableDevEnvironment(name, config, { hot: false }) + }, + }, + build: { + outDir: 'dist/rsc', + rollupOptions: { + input: { index: '/src/entry.rsc.tsx' }, + }, + }, + }, + }, + plugins: [ + { + name: 'ssr-middleware', + configureServer(server) { + const ssrRunner = (server.environments.ssr as RunnableDevEnvironment) + .runner + const rscRunner = (server.environments.rsc as RunnableDevEnvironment) + .runner + Object.assign(globalThis, { __rscRunner: rscRunner }) + return () => { + server.middlewares.use(async (req, res, next) => { + try { + const mod: any = await ssrRunner.import('/src/entry.ssr.tsx') + await mod.default(req, res) + } catch (e) { + next(e) + } + }) + } + }, + async configurePreviewServer(server) { + const mod = await import(path.resolve('dist/ssr/index.js')) + return () => { + server.middlewares.use(async (req, res, next) => { + try { + await mod.default(req, res) + } catch (e) { + next(e) + } + }) + } + }, + }, + { + name: 'virtual:build-rsc-entry', + resolveId(source) { + if (source === 'virtual:build-rsc-entry') { + // externalize rsc entry in ssr entry as relative path + return { id: '../rsc/index.js', external: true } + } + }, + }, + createVirtualPlugin('ssr-assets', function () { + assert(this.environment.name === 'ssr') + let bootstrapModules: string[] = [] + if (this.environment.mode === 'dev') { + bootstrapModules = ['/@id/__x00__virtual:browser-entry'] + } + if (this.environment.mode === 'build') { + bootstrapModules = [browserManifest['virtual:browser-entry'].file] + } + return `export const bootstrapModules = ${JSON.stringify(bootstrapModules)}` + }), + createVirtualPlugin('browser-entry', function () { + if (this.environment.mode === 'dev') { + return ` + import "/@vite/client"; + import RefreshRuntime from "/@react-refresh"; + RefreshRuntime.injectIntoGlobalHook(window); + window.$RefreshReg$ = () => {}; + window.$RefreshSig$ = () => (type) => type; + window.__vite_plugin_react_preamble_installed__ = true; + await import("/src/entry.client.tsx"); + ` + } else { + return `import "/src/entry.client.tsx";` + } + }), + { + name: 'misc', + hotUpdate(ctx) { + if (this.environment.name === 'rsc') { + const ids = ctx.modules.map((mod) => mod.id).filter((v) => v !== null) + if (ids.length > 0) { + // client reference id is also in react server module graph, + // but we skip RSC HMR for this case since Client HMR handles it. + if (!ids.some((id) => id in clientReferences)) { + ctx.server.environments.client.hot.send({ + type: 'custom', + event: 'react-server:update', + data: { + file: ctx.file, + }, + }) + } + } + } + }, + writeBundle(_options, bundle) { + if (this.environment.name === 'client') { + const output = bundle['.vite/manifest.json'] + assert(output.type === 'asset') + assert(typeof output.source === 'string') + browserManifest = JSON.parse(output.source) + } + }, + }, + vitePluginUseClient(), + vitePluginUseServer(), + vitePluginSilenceDirectiveBuildWarning(), + react(), + ], + builder: { + sharedPlugins: true, + async buildApp(builder) { + buildScan = true + await builder.build(builder.environments.rsc) + buildScan = false + await builder.build(builder.environments.rsc) + await builder.build(builder.environments.client) + await builder.build(builder.environments.ssr) + }, + }, +}) + +function vitePluginUseClient(): Plugin[] { + return [ + { + name: vitePluginUseClient.name, + transform(code, id) { + if (this.environment.name === 'rsc') { + if (/^(("use client")|('use client'))/.test(code)) { + // pass through client code to find server reference used only by client + if (buildScan) { + return + } + clientReferences[id] = id // TODO: normalize + const matches = [ + ...code.matchAll(/export function (\w+)\(/g), + ...code.matchAll(/export (default) (function|class) /g), + ] + const result = [ + `import $$ReactServer from "react-server-dom-vite/server"`, + ...[...matches].map( + ([, name]) => + `export ${name === 'default' ? 'default' : `const ${name} =`} $$ReactServer.registerClientReference({}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, + ), + ].join(';\n') + return { code: result, map: null } + } + } + }, + }, + createVirtualPlugin('build-client-references', () => { + const code = Object.keys(clientReferences) + .map( + (id) => `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, + ) + .join('\n') + return `export default {${code}}` + }), + ] +} + +function vitePluginUseServer(): Plugin[] { + return [ + { + name: vitePluginUseServer.name, + transform(code, id) { + if (/^(("use server")|('use server'))/.test(code)) { + serverReferences[id] = id + if (this.environment.name === 'rsc') { + const matches = code.matchAll(/export async function (\w+)\(/g) + const result = [ + code, + `import $$ReactServer from "react-server-dom-vite/server"`, + ...[...matches].map( + ([, name]) => + `${name} = $$ReactServer.registerServerReference(${name}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, + ), + ].join(';\n') + return { code: result, map: null } + } else { + const matches = code.matchAll(/export async function (\w+)\(/g) + const result = [ + `import $$ReactClient from "react-server-dom-vite/client"`, + ...[...matches].map( + ([, name]) => + `export const ${name} = $$ReactClient.createServerReference(${JSON.stringify(id + '#' + name)}, (...args) => __callServer(...args))`, + ), + ].join(';\n') + return { code: result, map: null } + } + } + }, + }, + createVirtualPlugin('build-server-references', () => { + const code = Object.keys(serverReferences) + .map( + (id) => `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, + ) + .join('\n') + return `export default {${code}}` + }), + ] +} + +function createVirtualPlugin(name: string, load: Plugin['load']) { + name = 'virtual:' + name + return { + name: `virtual-${name}`, + resolveId(source, _importer, _options) { + return source === name ? '\0' + name : undefined + }, + load(id, options) { + if (id === '\0' + name) { + return (load as Function).apply(this, [id, options]) + } + }, + } satisfies Plugin +} + +// silence warning due to "use ..." directives +// https://github.com/vitejs/vite-plugin-react/blob/814ed8043d321f4b4679a9f4a781d1ed14f185e4/packages/plugin-react/src/index.ts#L303 +function vitePluginSilenceDirectiveBuildWarning(): Plugin { + return { + name: vitePluginSilenceDirectiveBuildWarning.name, + enforce: 'post', + config(config, _env) { + return { + build: { + rollupOptions: { + onwarn(warning, defaultHandler) { + // https://github.com/vitejs/vite/issues/15012#issuecomment-1948550039 + if ( + warning.code === 'SOURCEMAP_ERROR' && + warning.message.includes('(1:0)') + ) { + return + } + // https://github.com/TanStack/query/pull/5161#issuecomment-1506683450 + if ( + warning.code === 'MODULE_LEVEL_DIRECTIVE' && + (warning.message.includes(`use client`) || + warning.message.includes(`use server`)) + ) { + return + } + if (config.build?.rollupOptions?.onwarn) { + config.build.rollupOptions.onwarn(warning, defaultHandler) + } else { + defaultHandler(warning) + } + }, + }, + }, + } + }, + } +} diff --git a/website/package.json b/website/package.json index 58d4d10..474d1e7 100644 --- a/website/package.json +++ b/website/package.json @@ -45,7 +45,7 @@ "rehype-mdx-import-media": "^1.2.0", "tailwindcss": "^3.4.3", "typescript": "^5.7.3", - "vite": "^6.0.11", + "vite": "^6.1.0", "vite-tsconfig-paths": "^4.2.1", "wrangler": "^3.48.0" }, From 420227ac2dd76e12a4dfc7598727d7b3232b4aaa Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 10:05:49 +0100 Subject: [PATCH 002/100] adding js extension --- spiceflow/scripts/example-app.ts | 2 +- spiceflow/scripts/openapi-fern.ts | 2 +- spiceflow/scripts/play-sdk.ts | 4 ++-- spiceflow/src/react/entry.client.tsx | 8 ++++---- spiceflow/src/react/entry.rsc.tsx | 6 +++--- spiceflow/src/react/entry.ssr.tsx | 8 ++++---- spiceflow/src/react/utils/client-reference.ts | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/spiceflow/scripts/example-app.ts b/spiceflow/scripts/example-app.ts index 8ffe8ae..2d20416 100644 --- a/spiceflow/scripts/example-app.ts +++ b/spiceflow/scripts/example-app.ts @@ -1,4 +1,4 @@ -import { Spiceflow } from '../src' +import { Spiceflow } from "../src.js" import { z } from 'zod' import { openapi } from '../src/openapi.js' diff --git a/spiceflow/scripts/openapi-fern.ts b/spiceflow/scripts/openapi-fern.ts index f4423eb..e9871ba 100644 --- a/spiceflow/scripts/openapi-fern.ts +++ b/spiceflow/scripts/openapi-fern.ts @@ -2,7 +2,7 @@ import fs from 'fs' import path from 'path' import yaml from 'js-yaml' -import { createSpiceflowClient } from '../src/client' +import { createSpiceflowClient } from "../src/client.js" import { app } from './example-app.js' async function main() { diff --git a/spiceflow/scripts/play-sdk.ts b/spiceflow/scripts/play-sdk.ts index dd9cb65..797bdb0 100644 --- a/spiceflow/scripts/play-sdk.ts +++ b/spiceflow/scripts/play-sdk.ts @@ -1,5 +1,5 @@ -import { app } from './example-app' -import { ExampleSdkClient } from './sdk-typescript' +import { app } from "./example-app.js" +import { ExampleSdkClient } from "./sdk-typescript.js" async function main() { const port = 3340 diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index b4c1229..21d1330 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,10 +1,10 @@ import React from "react"; import ReactDomClient from "react-dom/client"; import ReactClient from "react-server-dom-vite/client"; -import type { ServerPayload } from "./entry.rsc"; -import type { CallServerFn } from "./types"; -import { clientReferenceManifest } from "./utils/client-reference"; -import { getFlightStreamBrowser } from "./utils/stream-script"; +import type { ServerPayload } from "./entry.rsc.js"; +import type { CallServerFn } from "./types.js"; +import { clientReferenceManifest } from "./utils/client-reference.js"; +import { getFlightStreamBrowser } from "./utils/stream-script.js"; async function main() { const callServer: CallServerFn = async (id, args) => { diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index b5fea1e..1e22268 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -1,11 +1,11 @@ import type { ReactFormState } from "react-dom/client"; import ReactServer from "react-server-dom-vite/server"; -import { Router } from "./app/routes"; +import { Router } from "./app/routes.js"; import type { ClientReferenceMetadataManifest, ServerReferenceManifest, -} from "./types"; -import { fromPipeableToWebReadable } from "./utils/fetch"; +} from "./types.js"; +import { fromPipeableToWebReadable } from "./utils/fetch.js"; export interface RscHandlerResult { stream: ReadableStream; diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 1fc3726..25946cb 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -2,15 +2,15 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import ReactDomServer from "react-dom/server"; import ReactClient from "react-server-dom-vite/client"; import type { ModuleRunner } from "vite/module-runner"; -import type { ServerPayload } from "./entry.rsc"; -import { clientReferenceManifest } from "./utils/client-reference"; +import type { ServerPayload } from "./entry.rsc.js"; +import { clientReferenceManifest } from "./utils/client-reference.js"; import { createRequest, fromPipeableToWebReadable, fromWebToNodeReadable, sendResponse, -} from "./utils/fetch"; -import { injectFlightStream } from "./utils/stream-script"; +} from "./utils/fetch.js"; +import { injectFlightStream } from "./utils/stream-script.js"; export default async function handler( req: IncomingMessage, diff --git a/spiceflow/src/react/utils/client-reference.ts b/spiceflow/src/react/utils/client-reference.ts index efe4553..7795fd5 100644 --- a/spiceflow/src/react/utils/client-reference.ts +++ b/spiceflow/src/react/utils/client-reference.ts @@ -1,4 +1,4 @@ -import type { ClientReferenceManifest } from "../types"; +import type { ClientReferenceManifest } from "../types.js"; export const clientReferenceManifest: ClientReferenceManifest = { resolveClientReference(reference: string) { From f949eca1bad78bf9a3d4b458dc7f80eaa91cd43a Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 10:08:16 +0100 Subject: [PATCH 003/100] fix tsc errors --- pnpm-lock.yaml | 87 +++++-------------- spiceflow/package.json | 7 +- spiceflow/src/react/entry.client.tsx | 2 +- spiceflow/src/react/entry.rsc.tsx | 5 +- spiceflow/src/react/entry.ssr.tsx | 2 +- spiceflow/src/react/utils/client-reference.ts | 2 +- spiceflow/tsconfig.json | 1 + 7 files changed, 33 insertions(+), 73 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a89a7c..f9abdb1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -164,12 +164,21 @@ importers: openapi-types: specifier: ^12.1.3 version: 12.1.3 + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) react-server-dom-vite: specifier: npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14 version: '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0)' superjson: specifier: ^2.2.2 version: 2.2.2 + vite: + specifier: ^6.1.0 + version: 6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) zod: specifier: ^3.24.1 version: 3.24.1 @@ -206,7 +215,7 @@ importers: version: 3.1.0(@types/react@19.0.8)(react@19.0.0) '@mdx-js/rollup': specifier: ^3.1.0 - version: 3.1.0(acorn@8.14.0)(rollup@4.34.0) + version: 3.1.0(acorn@8.14.0)(rollup@4.34.6) '@remix-run/cloudflare': specifier: ^2.15.3 version: 2.15.3(@cloudflare/workers-types@4.20250129.0)(typescript@5.7.3) @@ -255,7 +264,7 @@ importers: version: 4.20250129.0 '@remix-run/dev': specifier: ^2.15.3 - version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) + version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -285,7 +294,7 @@ importers: version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.7.3)(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) wrangler: specifier: ^3.48.0 version: 3.107.2(@cloudflare/workers-types@4.20250129.0) @@ -5422,46 +5431,6 @@ packages: terser: optional: true - vite@6.0.11: - resolution: {integrity: sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - jiti: '>=1.21.0' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vite@6.1.0: resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -6702,11 +6671,11 @@ snapshots: '@types/react': 19.0.8 react: 19.0.0 - '@mdx-js/rollup@3.1.0(acorn@8.14.0)(rollup@4.34.0)': + '@mdx-js/rollup@3.1.0(acorn@8.14.0)(rollup@4.34.6)': dependencies: '@mdx-js/mdx': 3.1.0(acorn@8.14.0) - '@rollup/pluginutils': 5.1.4(rollup@4.34.0) - rollup: 4.34.0 + '@rollup/pluginutils': 5.1.4(rollup@4.34.6) + rollup: 4.34.6 source-map: 0.7.4 vfile: 6.0.3 transitivePeerDependencies: @@ -6790,7 +6759,7 @@ snapshots: optionalDependencies: typescript: 5.7.3 - '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': + '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': dependencies: '@babel/core': 7.26.7 '@babel/generator': 7.26.5 @@ -6850,7 +6819,7 @@ snapshots: ws: 7.5.10 optionalDependencies: typescript: 5.7.3 - vite: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) wrangler: 3.107.2(@cloudflare/workers-types@4.20250129.0) transitivePeerDependencies: - '@types/node' @@ -6934,13 +6903,13 @@ snapshots: dependencies: web-streams-polyfill: 3.3.3 - '@rollup/pluginutils@5.1.4(rollup@4.34.0)': + '@rollup/pluginutils@5.1.4(rollup@4.34.6)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.34.0 + rollup: 4.34.6 '@rollup/rollup-android-arm-eabi@4.34.0': optional: true @@ -11754,13 +11723,13 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@4.3.2(typescript@5.7.3)(vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): + vite-tsconfig-paths@4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.3) optionalDependencies: - vite: 6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript @@ -11775,20 +11744,6 @@ snapshots: fsevents: 2.3.3 terser: 5.31.6 - vite@6.0.11(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): - dependencies: - esbuild: 0.24.2 - postcss: 8.5.1 - rollup: 4.34.0 - optionalDependencies: - '@types/node': 22.13.0 - fsevents: 2.3.3 - jiti: 1.21.7 - terser: 5.31.6 - tsx: 4.19.2 - yaml: 2.7.0 - optional: true - vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 diff --git a/spiceflow/package.json b/spiceflow/package.json index 967c15f..9cdb59b 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -42,7 +42,7 @@ "fern-docs": "pnpm run gen-openapi && fern generate --docs --force", "play-sdk:build": "pnpm vite build --config ./scripts/play-sdk.vite.ts", "test": "pnpm vitest", - "prepare": "pnpm build", + "prepare": "# pnpm build", "watch": "tsc -w" }, "files": [ @@ -56,13 +56,16 @@ "@medley/router": "^0.2.1", "@sinclair/typebox": "^0.34.14", "@vitejs/plugin-react": "^4.3.4", - "react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "eventsource-parser": "^3.0.0", "lodash.clonedeep": "^4.5.0", "openapi-types": "^12.1.3", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14", "superjson": "^2.2.2", + "vite": "^6.1.0", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.1" }, diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 21d1330..c2414d5 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -2,7 +2,7 @@ import React from "react"; import ReactDomClient from "react-dom/client"; import ReactClient from "react-server-dom-vite/client"; import type { ServerPayload } from "./entry.rsc.js"; -import type { CallServerFn } from "./types.js"; +import type { CallServerFn } from "./types/index.js"; import { clientReferenceManifest } from "./utils/client-reference.js"; import { getFlightStreamBrowser } from "./utils/stream-script.js"; diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index 1e22268..207d62d 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -1,10 +1,11 @@ import type { ReactFormState } from "react-dom/client"; +import React from "react"; import ReactServer from "react-server-dom-vite/server"; -import { Router } from "./app/routes.js"; + import type { ClientReferenceMetadataManifest, ServerReferenceManifest, -} from "./types.js"; +} from "./types/index.js"; import { fromPipeableToWebReadable } from "./utils/fetch.js"; export interface RscHandlerResult { diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 25946cb..1664732 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -63,7 +63,7 @@ export default async function handler( declare let __rscRunner: ModuleRunner; -async function importRscEntry(): Promise { +async function importRscEntry(): Promise { if (import.meta.env.DEV) { return await __rscRunner.import("/src/entry.rsc.tsx"); } else { diff --git a/spiceflow/src/react/utils/client-reference.ts b/spiceflow/src/react/utils/client-reference.ts index 7795fd5..4517f3f 100644 --- a/spiceflow/src/react/utils/client-reference.ts +++ b/spiceflow/src/react/utils/client-reference.ts @@ -1,4 +1,4 @@ -import type { ClientReferenceManifest } from "../types.js"; +import type { ClientReferenceManifest } from "../types/index.js"; export const clientReferenceManifest: ClientReferenceManifest = { resolveClientReference(reference: string) { diff --git a/spiceflow/tsconfig.json b/spiceflow/tsconfig.json index 6f9eef8..9c18dab 100644 --- a/spiceflow/tsconfig.json +++ b/spiceflow/tsconfig.json @@ -8,6 +8,7 @@ "moduleResolution": "NodeNext", "declarationMap": true, "sourceMap": true, + "jsx": "react-jsx", "resolveJsonModule": true, "outDir": "dist" }, From 50edca0a8a20f674372336689b3d86bfe568b930 Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 10:23:15 +0100 Subject: [PATCH 004/100] making vite plugin --- example-react/package.json | 18 ++ example-react/src/main.tsx | 10 + example-react/vite.config.ts | 11 + pnpm-lock.yaml | 15 ++ spiceflow/src/react/entry.rsc.tsx | 4 + spiceflow/src/vite.tsx | 407 ++++++++++++++++-------------- 6 files changed, 269 insertions(+), 196 deletions(-) create mode 100644 example-react/package.json create mode 100644 example-react/src/main.tsx create mode 100644 example-react/vite.config.ts diff --git a/example-react/package.json b/example-react/package.json new file mode 100644 index 0000000..802b8dd --- /dev/null +++ b/example-react/package.json @@ -0,0 +1,18 @@ +{ + "name": "example-react", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "vite" + }, + "keywords": [], + "author": "remorses ", + "dependencies": { + "react": "19.0.0", + "react-dom": "19.0.0", + "spiceflow": "workspace:*", + "vite": "^6.1.0" + }, + "license": "ISC" +} diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx new file mode 100644 index 0000000..8bbfcb8 --- /dev/null +++ b/example-react/src/main.tsx @@ -0,0 +1,10 @@ +import { Spiceflow } from 'spiceflow' + +const app = new Spiceflow() + .get('/hello', () => 'Hello, World!') + .post('/echo', async ({ request }) => { + const body = await request.json() + return { echo: body } + }) + +export default app diff --git a/example-react/vite.config.ts b/example-react/vite.config.ts new file mode 100644 index 0000000..9856ca1 --- /dev/null +++ b/example-react/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite' +import { spiceflowPlugin } from 'spiceflow/dist/vite' + +export default defineConfig({ + clearScreen: false, + plugins: [ + spiceflowPlugin({ + entry: './src/main.tsx', + }), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9abdb1..c61565f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,6 +37,21 @@ importers: specifier: ^3.0.4 version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + example-react: + dependencies: + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + spiceflow: + specifier: workspace:* + version: link:../spiceflow + vite: + specifier: ^6.1.0 + version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + openapi-schema-diff: dependencies: json-schema-ref-resolver: diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index 207d62d..826ae45 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -18,6 +18,10 @@ export interface ServerPayload { returnValue?: unknown; } +function Router() { + return
hello world
+} + export async function handler( url: URL, request: Request, diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index e62b2ed..bba381c 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -1,70 +1,90 @@ import assert from 'node:assert' +import url from 'node:url' import path from 'node:path' import react from '@vitejs/plugin-react' import { type Manifest, type Plugin, + PluginOption, type RunnableDevEnvironment, createRunnableDevEnvironment, defineConfig, } from 'vite' -// state for build orchestration -let browserManifest: Manifest -let clientReferences: Record = {} // TODO: normalize id -let serverReferences: Record = {} -let buildScan = false +export function spiceflowPlugin({ entry }): PluginOption { + // Move state variables inside plugin closure + let browserManifest: Manifest + let clientReferences: Record = {} // TODO: normalize id + let serverReferences: Record = {} + let buildScan = false -export default defineConfig({ - appType: 'custom', - environments: { - client: { - optimizeDeps: { - include: ['react-dom/client', 'react-server-dom-vite/client'], - }, - build: { - manifest: true, - outDir: 'dist/client', - rollupOptions: { - input: { index: 'virtual:browser-entry' }, - }, - }, - }, - ssr: { - build: { - outDir: 'dist/ssr', - rollupOptions: { - input: { index: '/src/entry.ssr.tsx' }, - }, - }, - }, - rsc: { - optimizeDeps: { - include: [ - 'react', - 'react/jsx-runtime', - 'react/jsx-dev-runtime', - 'react-server-dom-vite/server', - ], - }, - resolve: { - conditions: ['react-server'], - noExternal: true, - }, - dev: { - createEnvironment(name, config) { - return createRunnableDevEnvironment(name, config, { hot: false }) + return [ + { + name: 'spiceflow', + config: () => ({ + appType: 'custom', + environments: { + client: { + optimizeDeps: { + include: ['react-dom/client', 'react-server-dom-vite/client'], + }, + build: { + manifest: true, + outDir: 'dist/client', + rollupOptions: { + input: { index: 'virtual:browser-entry' }, + }, + }, + }, + ssr: { + build: { + outDir: 'dist/ssr', + rollupOptions: { + input: { index: '/src/entry.ssr.tsx' }, + }, + }, + }, + rsc: { + optimizeDeps: { + include: [ + 'react', + 'react/jsx-runtime', + 'react/jsx-dev-runtime', + 'react-server-dom-vite/server', + ], + }, + resolve: { + conditions: ['react-server'], + noExternal: true, + }, + dev: { + createEnvironment(name, config) { + return createRunnableDevEnvironment(name, config, { + hot: false, + }) + }, + }, + build: { + outDir: 'dist/rsc', + rollupOptions: { + input: { index: '/src/entry.rsc.tsx' }, + }, + }, + }, }, - }, - build: { - outDir: 'dist/rsc', - rollupOptions: { - input: { index: '/src/entry.rsc.tsx' }, + builder: { + sharedPlugins: true, + async buildApp(builder) { + buildScan = true + await builder.build(builder.environments.rsc) + buildScan = false + await builder.build(builder.environments.rsc) + await builder.build(builder.environments.client) + await builder.build(builder.environments.ssr) + }, }, - }, + }), }, - }, - plugins: [ { name: 'ssr-middleware', configureServer(server) { @@ -106,6 +126,10 @@ export default defineConfig({ } }, }, + createVirtualPlugin('app-entry', () => { + return `export * from '${url.pathToFileURL(path.resolve(entry))}'` + }), + createVirtualPlugin('ssr-assets', function () { assert(this.environment.name === 'ssr') let bootstrapModules: string[] = [] @@ -120,14 +144,14 @@ export default defineConfig({ createVirtualPlugin('browser-entry', function () { if (this.environment.mode === 'dev') { return ` - import "/@vite/client"; - import RefreshRuntime from "/@react-refresh"; - RefreshRuntime.injectIntoGlobalHook(window); - window.$RefreshReg$ = () => {}; - window.$RefreshSig$ = () => (type) => type; - window.__vite_plugin_react_preamble_installed__ = true; - await import("/src/entry.client.tsx"); - ` + import "/@vite/client"; + import RefreshRuntime from "/@react-refresh"; + RefreshRuntime.injectIntoGlobalHook(window); + window.$RefreshReg$ = () => {}; + window.$RefreshSig$ = () => (type) => type; + window.__vite_plugin_react_preamble_installed__ = true; + await import("/src/entry.client.tsx"); + ` } else { return `import "/src/entry.client.tsx";` } @@ -165,152 +189,143 @@ export default defineConfig({ vitePluginUseServer(), vitePluginSilenceDirectiveBuildWarning(), react(), - ], - builder: { - sharedPlugins: true, - async buildApp(builder) { - buildScan = true - await builder.build(builder.environments.rsc) - buildScan = false - await builder.build(builder.environments.rsc) - await builder.build(builder.environments.client) - await builder.build(builder.environments.ssr) - }, - }, -}) + ] -function vitePluginUseClient(): Plugin[] { - return [ - { - name: vitePluginUseClient.name, - transform(code, id) { - if (this.environment.name === 'rsc') { - if (/^(("use client")|('use client'))/.test(code)) { - // pass through client code to find server reference used only by client - if (buildScan) { - return + function vitePluginUseClient(): Plugin[] { + return [ + { + name: vitePluginUseClient.name, + transform(code, id) { + if (this.environment.name === 'rsc') { + if (/^(("use client")|('use client'))/.test(code)) { + // pass through client code to find server reference used only by client + if (buildScan) { + return + } + clientReferences[id] = id // TODO: normalize + const matches = [ + ...code.matchAll(/export function (\w+)\(/g), + ...code.matchAll(/export (default) (function|class) /g), + ] + const result = [ + `import $$ReactServer from "react-server-dom-vite/server"`, + ...[...matches].map( + ([, name]) => + `export ${name === 'default' ? 'default' : `const ${name} =`} $$ReactServer.registerClientReference({}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, + ), + ].join(';\n') + return { code: result, map: null } } - clientReferences[id] = id // TODO: normalize - const matches = [ - ...code.matchAll(/export function (\w+)\(/g), - ...code.matchAll(/export (default) (function|class) /g), - ] - const result = [ - `import $$ReactServer from "react-server-dom-vite/server"`, - ...[...matches].map( - ([, name]) => - `export ${name === 'default' ? 'default' : `const ${name} =`} $$ReactServer.registerClientReference({}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, - ), - ].join(';\n') - return { code: result, map: null } } - } + }, }, - }, - createVirtualPlugin('build-client-references', () => { - const code = Object.keys(clientReferences) - .map( - (id) => `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, - ) - .join('\n') - return `export default {${code}}` - }), - ] -} + createVirtualPlugin('build-client-references', () => { + const code = Object.keys(clientReferences) + .map( + (id) => + `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, + ) + .join('\n') + return `export default {${code}}` + }), + ] + } -function vitePluginUseServer(): Plugin[] { - return [ - { - name: vitePluginUseServer.name, - transform(code, id) { - if (/^(("use server")|('use server'))/.test(code)) { - serverReferences[id] = id - if (this.environment.name === 'rsc') { - const matches = code.matchAll(/export async function (\w+)\(/g) - const result = [ - code, - `import $$ReactServer from "react-server-dom-vite/server"`, - ...[...matches].map( - ([, name]) => - `${name} = $$ReactServer.registerServerReference(${name}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, - ), - ].join(';\n') - return { code: result, map: null } - } else { - const matches = code.matchAll(/export async function (\w+)\(/g) - const result = [ - `import $$ReactClient from "react-server-dom-vite/client"`, - ...[...matches].map( - ([, name]) => - `export const ${name} = $$ReactClient.createServerReference(${JSON.stringify(id + '#' + name)}, (...args) => __callServer(...args))`, - ), - ].join(';\n') - return { code: result, map: null } + function vitePluginUseServer(): Plugin[] { + return [ + { + name: vitePluginUseServer.name, + transform(code, id) { + if (/^(("use server")|('use server'))/.test(code)) { + serverReferences[id] = id + if (this.environment.name === 'rsc') { + const matches = code.matchAll(/export async function (\w+)\(/g) + const result = [ + code, + `import $$ReactServer from "react-server-dom-vite/server"`, + ...[...matches].map( + ([, name]) => + `${name} = $$ReactServer.registerServerReference(${name}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, + ), + ].join(';\n') + return { code: result, map: null } + } else { + const matches = code.matchAll(/export async function (\w+)\(/g) + const result = [ + `import $$ReactClient from "react-server-dom-vite/client"`, + ...[...matches].map( + ([, name]) => + `export const ${name} = $$ReactClient.createServerReference(${JSON.stringify(id + '#' + name)}, (...args) => __callServer(...args))`, + ), + ].join(';\n') + return { code: result, map: null } + } } - } + }, }, - }, - createVirtualPlugin('build-server-references', () => { - const code = Object.keys(serverReferences) - .map( - (id) => `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, - ) - .join('\n') - return `export default {${code}}` - }), - ] -} + createVirtualPlugin('build-server-references', () => { + const code = Object.keys(serverReferences) + .map( + (id) => + `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, + ) + .join('\n') + return `export default {${code}}` + }), + ] + } -function createVirtualPlugin(name: string, load: Plugin['load']) { - name = 'virtual:' + name - return { - name: `virtual-${name}`, - resolveId(source, _importer, _options) { - return source === name ? '\0' + name : undefined - }, - load(id, options) { - if (id === '\0' + name) { - return (load as Function).apply(this, [id, options]) - } - }, - } satisfies Plugin -} + function createVirtualPlugin(name: string, load: Plugin['load']) { + name = 'virtual:' + name + return { + name: `virtual-${name}`, + resolveId(source, _importer, _options) { + return source === name ? '\0' + name : undefined + }, + load(id, options) { + if (id === '\0' + name) { + return (load as Function).apply(this, [id, options]) + } + }, + } satisfies Plugin + } -// silence warning due to "use ..." directives -// https://github.com/vitejs/vite-plugin-react/blob/814ed8043d321f4b4679a9f4a781d1ed14f185e4/packages/plugin-react/src/index.ts#L303 -function vitePluginSilenceDirectiveBuildWarning(): Plugin { - return { - name: vitePluginSilenceDirectiveBuildWarning.name, - enforce: 'post', - config(config, _env) { - return { - build: { - rollupOptions: { - onwarn(warning, defaultHandler) { - // https://github.com/vitejs/vite/issues/15012#issuecomment-1948550039 - if ( - warning.code === 'SOURCEMAP_ERROR' && - warning.message.includes('(1:0)') - ) { - return - } - // https://github.com/TanStack/query/pull/5161#issuecomment-1506683450 - if ( - warning.code === 'MODULE_LEVEL_DIRECTIVE' && - (warning.message.includes(`use client`) || - warning.message.includes(`use server`)) - ) { - return - } - if (config.build?.rollupOptions?.onwarn) { - config.build.rollupOptions.onwarn(warning, defaultHandler) - } else { - defaultHandler(warning) - } + // silence warning due to "use ..." directives + // https://github.com/vitejs/vite-plugin-react/blob/814ed8043d321f4b4679a9f4a781d1ed14f185e4/packages/plugin-react/src/index.ts#L303 + function vitePluginSilenceDirectiveBuildWarning(): Plugin { + return { + name: vitePluginSilenceDirectiveBuildWarning.name, + enforce: 'post', + config(config, _env) { + return { + build: { + rollupOptions: { + onwarn(warning, defaultHandler) { + // https://github.com/vitejs/vite/issues/15012#issuecomment-1948550039 + if ( + warning.code === 'SOURCEMAP_ERROR' && + warning.message.includes('(1:0)') + ) { + return + } + // https://github.com/TanStack/query/pull/5161#issuecomment-1506683450 + if ( + warning.code === 'MODULE_LEVEL_DIRECTIVE' && + (warning.message.includes(`use client`) || + warning.message.includes(`use server`)) + ) { + return + } + if (config.build?.rollupOptions?.onwarn) { + config.build.rollupOptions.onwarn(warning, defaultHandler) + } else { + defaultHandler(warning) + } + }, }, }, - }, - } - }, + } + }, + } } } From e4b82f1e2d7841a09df44bfaa088a1201b8438d1 Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 11:16:14 +0100 Subject: [PATCH 005/100] it works, vite optimize deps thing is awful, added util in exclude or the react server would be optimized for browser --- pnpm-lock.yaml | 27 +++++++++++++++++++- spiceflow/package.json | 4 +++ spiceflow/src/react/entry.rsc.tsx | 2 +- spiceflow/src/react/entry.ssr.tsx | 2 +- spiceflow/src/react/server-dom-optimized.tsx | 3 +++ spiceflow/src/vite.tsx | 22 ++++++++++------ 6 files changed, 49 insertions(+), 11 deletions(-) create mode 100644 spiceflow/src/react/server-dom-optimized.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c61565f..cf556a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,6 +188,9 @@ importers: react-server-dom-vite: specifier: npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14 version: '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0)' + spiceflow: + specifier: '*' + version: 1.6.1(@modelcontextprotocol/sdk@1.0.4) superjson: specifier: ^2.2.2 version: 2.2.2 @@ -4898,6 +4901,14 @@ packages: spdx-license-ids@3.0.21: resolution: {integrity: sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==} + spiceflow@1.6.1: + resolution: {integrity: sha512-PBJ4QC/RjBIoNKVBALDAQZkTF7KG94xV7rmSil9HXi0kXh4k+akaI7pFG15pTMPfX7EZPQRo/3kml9tSEeJ9gw==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.0.4 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -7285,7 +7296,7 @@ snapshots: '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.7) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.1 - esbuild: 0.17.19 + esbuild: 0.17.6 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 @@ -11086,6 +11097,20 @@ snapshots: spdx-license-ids@3.0.21: {} + spiceflow@1.6.1(@modelcontextprotocol/sdk@1.0.4): + dependencies: + '@medley/router': 0.2.1 + '@sinclair/typebox': 0.34.15 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + eventsource-parser: 3.0.0 + openapi-types: 12.1.3 + superjson: 2.2.2 + zod: 3.24.1 + zod-to-json-schema: 3.24.1(zod@3.24.1) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.0.4 + sprintf-js@1.0.3: {} ssri@10.0.6: diff --git a/spiceflow/package.json b/spiceflow/package.json index 9cdb59b..0574054 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -27,6 +27,9 @@ "types": "./dist/openapi.d.ts", "default": "./dist/openapi.js" }, + "./src/*": { + "default": "./src/*" + }, "./dist/*": { "types": "./dist/*.d.ts", "default": "./dist/*.js" @@ -67,6 +70,7 @@ "superjson": "^2.2.2", "vite": "^6.1.0", "zod": "^3.24.1", + "spiceflow": "*", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index 826ae45..35533ac 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -1,6 +1,6 @@ import type { ReactFormState } from "react-dom/client"; import React from "react"; -import ReactServer from "react-server-dom-vite/server"; +import ReactServer from "spiceflow/dist/react/server-dom-optimized"; import type { ClientReferenceMetadataManifest, diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 1664732..7418def 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -65,7 +65,7 @@ declare let __rscRunner: ModuleRunner; async function importRscEntry(): Promise { if (import.meta.env.DEV) { - return await __rscRunner.import("/src/entry.rsc.tsx"); + return await __rscRunner.import("spiceflow/src/react/entry.rsc.tsx"); } else { return await import("virtual:build-rsc-entry" as any); } diff --git a/spiceflow/src/react/server-dom-optimized.tsx b/spiceflow/src/react/server-dom-optimized.tsx new file mode 100644 index 0000000..c391398 --- /dev/null +++ b/spiceflow/src/react/server-dom-optimized.tsx @@ -0,0 +1,3 @@ + +import ReactServer from 'react-server-dom-vite/server' +export default ReactServer \ No newline at end of file diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index bba381c..879915d 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -10,6 +10,9 @@ import { createRunnableDevEnvironment, defineConfig, } from 'vite' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) export function spiceflowPlugin({ entry }): PluginOption { // Move state variables inside plugin closure @@ -40,7 +43,7 @@ export function spiceflowPlugin({ entry }): PluginOption { build: { outDir: 'dist/ssr', rollupOptions: { - input: { index: '/src/entry.ssr.tsx' }, + input: { index: 'spiceflow/src/react/entry.ssr.tsx' }, }, }, }, @@ -50,8 +53,9 @@ export function spiceflowPlugin({ entry }): PluginOption { 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime', - 'react-server-dom-vite/server', + 'spiceflow/dist/react/server-dom-optimized', ], + exclude: ['util'], }, resolve: { conditions: ['react-server'], @@ -67,7 +71,7 @@ export function spiceflowPlugin({ entry }): PluginOption { build: { outDir: 'dist/rsc', rollupOptions: { - input: { index: '/src/entry.rsc.tsx' }, + input: { index: 'spiceflow/src/react/entry.rsc.tsx' }, }, }, }, @@ -96,7 +100,9 @@ export function spiceflowPlugin({ entry }): PluginOption { return () => { server.middlewares.use(async (req, res, next) => { try { - const mod: any = await ssrRunner.import('/src/entry.ssr.tsx') + const mod: any = await ssrRunner.import( + 'spiceflow/src/react/entry.ssr.tsx', + ) await mod.default(req, res) } catch (e) { next(e) @@ -150,10 +156,10 @@ export function spiceflowPlugin({ entry }): PluginOption { window.$RefreshReg$ = () => {}; window.$RefreshSig$ = () => (type) => type; window.__vite_plugin_react_preamble_installed__ = true; - await import("/src/entry.client.tsx"); + await import("spiceflow/src/react/entry.client.tsx"); ` } else { - return `import "/src/entry.client.tsx";` + return `import "spiceflow/src/react/entry.client.tsx";` } }), { @@ -208,7 +214,7 @@ export function spiceflowPlugin({ entry }): PluginOption { ...code.matchAll(/export (default) (function|class) /g), ] const result = [ - `import $$ReactServer from "react-server-dom-vite/server"`, + `import $$ReactServer from "spiceflow/dist/react/server-dom-optimized"`, ...[...matches].map( ([, name]) => `export ${name === 'default' ? 'default' : `const ${name} =`} $$ReactServer.registerClientReference({}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, @@ -242,7 +248,7 @@ export function spiceflowPlugin({ entry }): PluginOption { const matches = code.matchAll(/export async function (\w+)\(/g) const result = [ code, - `import $$ReactServer from "react-server-dom-vite/server"`, + `import $$ReactServer from "spiceflow/dist/react/server-dom-optimized"`, ...[...matches].map( ([, name]) => `${name} = $$ReactServer.registerServerReference(${name}, ${JSON.stringify(id)}, ${JSON.stringify(name)})`, From 76698adcdd429f0c23875e1138dac8493ac3dda3 Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 11:35:42 +0100 Subject: [PATCH 006/100] nn --- spiceflow/src/react/entry.ssr.tsx | 3 +++ spiceflow/src/vite.tsx | 3 +++ 2 files changed, 6 insertions(+) diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 7418def..b547019 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -12,6 +12,9 @@ import { } from "./utils/fetch.js"; import { injectFlightStream } from "./utils/stream-script.js"; + + + export default async function handler( req: IncomingMessage, res: ServerResponse, diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 879915d..054d2cd 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -8,6 +8,7 @@ import { PluginOption, type RunnableDevEnvironment, createRunnableDevEnvironment, + createServerModuleRunner, defineConfig, } from 'vite' import { fileURLToPath } from 'node:url' @@ -48,6 +49,7 @@ export function spiceflowPlugin({ entry }): PluginOption { }, }, rsc: { + optimizeDeps: { include: [ 'react', @@ -65,6 +67,7 @@ export function spiceflowPlugin({ entry }): PluginOption { createEnvironment(name, config) { return createRunnableDevEnvironment(name, config, { hot: false, + }) }, }, From 2dc1480b7f2e50f7b731c58e2ae9bba47961f71e Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 11:58:40 +0100 Subject: [PATCH 007/100] spiceflow works, removed noExternal because commonjs does not work that way --- example-react/src/main.tsx | 1 + spiceflow/src/react/entry.rsc.tsx | 17 +++++++++++------ spiceflow/src/react/entry.ssr.tsx | 5 +++++ spiceflow/src/react/types/ambient.d.ts | 7 +++++++ spiceflow/src/vite.tsx | 19 ++++++++++--------- spiceflow/tsconfig.json | 1 + 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 8bbfcb8..0940f57 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -1,6 +1,7 @@ import { Spiceflow } from 'spiceflow' const app = new Spiceflow() + .get('/', () => 'Hello, World!') .get('/hello', () => 'Hello, World!') .post('/echo', async ({ request }) => { const body = await request.json() diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index 35533ac..dca8e3e 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -6,6 +6,7 @@ import type { ClientReferenceMetadataManifest, ServerReferenceManifest, } from "./types/index.js"; +import app from 'virtual:app-entry' import { fromPipeableToWebReadable } from "./utils/fetch.js"; export interface RscHandlerResult { @@ -18,14 +19,11 @@ export interface ServerPayload { returnValue?: unknown; } -function Router() { - return
hello world
-} export async function handler( url: URL, request: Request, -): Promise { +) { // handle action let returnValue: unknown | undefined; let formState: ReactFormState | undefined; @@ -58,11 +56,17 @@ export async function handler( } } + const root = await app.handle(request) + + if (root instanceof Response) { + return root + } + // render flight stream const stream = fromPipeableToWebReadable( ReactServer.renderToPipeableStream( { - root: , + root, returnValue, formState, }, @@ -71,9 +75,10 @@ export async function handler( ), ); - return { + let r : RscHandlerResult = { stream, }; + return r } const serverReferenceManifest: ServerReferenceManifest = { diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index b547019..f6bbb21 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -24,6 +24,11 @@ export default async function handler( const rscEntry = await importRscEntry(); const rscResult = await rscEntry.handler(url, request); + if (rscResult instanceof Response) { + sendResponse(rscResult, res); + return; + } + if (url.searchParams.has("__rsc")) { const response = new Response(rscResult.stream, { headers: { diff --git a/spiceflow/src/react/types/ambient.d.ts b/spiceflow/src/react/types/ambient.d.ts index 2414f28..75d9c2a 100644 --- a/spiceflow/src/react/types/ambient.d.ts +++ b/spiceflow/src/react/types/ambient.d.ts @@ -1,4 +1,5 @@ /// +// import {Spiceflow} from '../../spiceflow.js' declare module "react-server-dom-vite/server" { export function renderToPipeableStream( @@ -50,6 +51,12 @@ declare module "virtual:ssr-assets" { export const bootstrapModules: string[]; } +declare module "virtual:app-entry" { + import type { Spiceflow } from "spiceflow"; + const app: Spiceflow; + export default app +} + declare module "virtual:build-client-references" { const value: Record Promise>>; export default value; diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 054d2cd..727b9bc 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -61,18 +61,19 @@ export function spiceflowPlugin({ entry }): PluginOption { }, resolve: { conditions: ['react-server'], - noExternal: true, + // noExternal: true, }, - dev: { - createEnvironment(name, config) { - return createRunnableDevEnvironment(name, config, { - hot: false, + // dev: { + // createEnvironment(name, config) { + // return createRunnableDevEnvironment(name, config, { + // hot: false, - }) - }, - }, + // }) + // }, + // }, build: { outDir: 'dist/rsc', + ssr: true, rollupOptions: { input: { index: 'spiceflow/src/react/entry.rsc.tsx' }, }, @@ -136,7 +137,7 @@ export function spiceflowPlugin({ entry }): PluginOption { }, }, createVirtualPlugin('app-entry', () => { - return `export * from '${url.pathToFileURL(path.resolve(entry))}'` + return `export {default} from '${url.pathToFileURL(path.resolve(entry))}'` }), createVirtualPlugin('ssr-assets', function () { diff --git a/spiceflow/tsconfig.json b/spiceflow/tsconfig.json index 9c18dab..90a6a11 100644 --- a/spiceflow/tsconfig.json +++ b/spiceflow/tsconfig.json @@ -2,6 +2,7 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "rootDir": "src", + "typeRoots": ["./src/react/types/ambient.d.ts"], "module": "NodeNext", "target": "ESNext", "lib": ["DOM", "ESNext", "dom.iterable"], From c9b94b6293e1a059daaa27364c5dfa06aafb51ad Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 12:16:02 +0100 Subject: [PATCH 008/100] kind of works, adding example app in react and react method --- example-react/src/app/action-by-client.tsx | 14 +++++ example-react/src/app/action.tsx | 12 ++++ example-react/src/app/client.tsx | 62 +++++++++++++++++++ example-react/src/app/index.tsx | 28 +++++++++ example-react/src/app/layout.tsx | 25 ++++++++ example-react/src/app/other.tsx | 3 + example-react/src/main.tsx | 6 +- example-react/tsconfig.json | 27 ++++++++ pnpm-lock.yaml | 5 +- spiceflow/package.json | 3 +- spiceflow/src/react/entry.client.tsx | 2 +- spiceflow/src/react/entry.ssr.tsx | 3 +- .../src/react/server-dom-client-optimized.tsx | 3 + spiceflow/src/react/types/ambient.d.ts | 2 +- spiceflow/src/spiceflow.ts | 60 +++++++++++++++++- spiceflow/src/vite.tsx | 4 +- spiceflow/tsconfig.json | 1 - 17 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 example-react/src/app/action-by-client.tsx create mode 100644 example-react/src/app/action.tsx create mode 100644 example-react/src/app/client.tsx create mode 100644 example-react/src/app/index.tsx create mode 100644 example-react/src/app/layout.tsx create mode 100644 example-react/src/app/other.tsx create mode 100644 example-react/tsconfig.json create mode 100644 spiceflow/src/react/server-dom-client-optimized.tsx diff --git a/example-react/src/app/action-by-client.tsx b/example-react/src/app/action-by-client.tsx new file mode 100644 index 0000000..559f302 --- /dev/null +++ b/example-react/src/app/action-by-client.tsx @@ -0,0 +1,14 @@ +"use server"; + +export async function add(_prev: unknown, formData: FormData) { + let x = formData.get("x"); + let y = formData.get("y"); + if (typeof x === "string" && typeof y === "string") { + let x2 = parseFloat(x); + let y2 = parseFloat(y); + if (!Number.isNaN(x2) && !Number.isNaN(y2)) { + return x2 + y2; + } + } + return "(invalid input)"; +} diff --git a/example-react/src/app/action.tsx b/example-react/src/app/action.tsx new file mode 100644 index 0000000..4920b18 --- /dev/null +++ b/example-react/src/app/action.tsx @@ -0,0 +1,12 @@ +"use server"; + +let counter = 0; + +export function getCounter() { + return counter; +} + +export async function changeCounter(formData: FormData) { + const change = Number(formData.get("change")); + counter += change; +} diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx new file mode 100644 index 0000000..5f63437 --- /dev/null +++ b/example-react/src/app/client.tsx @@ -0,0 +1,62 @@ +"use client"; + +import React from "react"; +import { add } from "./action-by-client"; + +export function Counter() { + const [count, setCount] = React.useState(0); + return ( +
+
Client counter: {count}
+
+ + +
+
+ ); +} + +export function Hydrated() { + return
[hydrated: {Number(useHydrated())}]
; +} + +function useHydrated() { + return React.useSyncExternalStore( + React.useCallback(() => () => {}, []), + () => true, + () => false, + ); +} + +export function Calculator() { + const [returnValue, formAction, _isPending] = React.useActionState(add, null); + const [x, setX] = React.useState(""); + const [y, setY] = React.useState(""); + + return ( +
+
Calculator
+
+ setX(e.target.value)} + /> + + + setY(e.target.value)} + /> + ={returnValue ?? "?"} +
+ +
+ ); +} diff --git a/example-react/src/app/index.tsx b/example-react/src/app/index.tsx new file mode 100644 index 0000000..268655d --- /dev/null +++ b/example-react/src/app/index.tsx @@ -0,0 +1,28 @@ +import { changeCounter, getCounter } from "./action"; +import { Calculator, Counter, Hydrated } from "./client"; + +export async function IndexPage() { + return ( +
+
server random: {Math.random().toString(36).slice(2)}
+ + +
+
Server counter: {getCounter()}
+
+ + +
+
+ +
+ ); +} diff --git a/example-react/src/app/layout.tsx b/example-react/src/app/layout.tsx new file mode 100644 index 0000000..f6009c5 --- /dev/null +++ b/example-react/src/app/layout.tsx @@ -0,0 +1,25 @@ +export function Layout(props: React.PropsWithChildren) { + return ( + + + + react-server + + + + + {props.children} + + + ); +} diff --git a/example-react/src/app/other.tsx b/example-react/src/app/other.tsx new file mode 100644 index 0000000..d7a7b25 --- /dev/null +++ b/example-react/src/app/other.tsx @@ -0,0 +1,3 @@ +export default function OtherPage() { + return
Other Page
; +} diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 0940f57..415df78 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -1,7 +1,11 @@ import { Spiceflow } from 'spiceflow' +import { IndexPage } from './app/index' const app = new Spiceflow() - .get('/', () => 'Hello, World!') + .react('/', ({ request}) => { + const url = new URL(request.url) + return + }) .get('/hello', () => 'Hello, World!') .post('/echo', async ({ request }) => { const body = await request.json() diff --git a/example-react/tsconfig.json b/example-react/tsconfig.json new file mode 100644 index 0000000..28d3155 --- /dev/null +++ b/example-react/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022", "esnext"], + "types": [ + "vite/client", + ], + "rootDir": "src", + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "target": "ES2022", + "strict": true, + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": ["./app/*"] + }, + "noImplicitAny": false, + // Vite takes care of building everything, not tsc. + "noEmit": true + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cf556a7..67cb941 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,6 +210,9 @@ importers: '@types/node': specifier: 22.12.0 version: 22.12.0 + '@types/react': + specifier: ^19.0.8 + version: 19.0.8 eventsource: specifier: ^3.0.5 version: 3.0.5 @@ -7296,7 +7299,7 @@ snapshots: '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.7) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.1 - esbuild: 0.17.6 + esbuild: 0.17.19 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 diff --git a/spiceflow/package.json b/spiceflow/package.json index 0574054..b30ea42 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -67,10 +67,10 @@ "react": "19.0.0", "react-dom": "19.0.0", "react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14", + "spiceflow": "*", "superjson": "^2.2.2", "vite": "^6.1.0", "zod": "^3.24.1", - "spiceflow": "*", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { @@ -84,6 +84,7 @@ "devDependencies": { "@types/lodash.clonedeep": "^4.5.9", "@types/node": "22.12.0", + "@types/react": "^19.0.8", "eventsource": "^3.0.5", "formdata-node": "^6.0.3", "js-base64": "^3.7.7", diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index c2414d5..c453f2a 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,6 +1,6 @@ import React from "react"; import ReactDomClient from "react-dom/client"; -import ReactClient from "react-server-dom-vite/client"; +import ReactClient from "spiceflow/dist/react/server-dom-client-optimized"; import type { ServerPayload } from "./entry.rsc.js"; import type { CallServerFn } from "./types/index.js"; import { clientReferenceManifest } from "./utils/client-reference.js"; diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index f6bbb21..5f90b85 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -1,6 +1,6 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import ReactDomServer from "react-dom/server"; -import ReactClient from "react-server-dom-vite/client"; +import ReactClient from "spiceflow/dist/react/server-dom-client-optimized"; import type { ModuleRunner } from "vite/module-runner"; import type { ServerPayload } from "./entry.rsc.js"; import { clientReferenceManifest } from "./utils/client-reference.js"; @@ -28,6 +28,7 @@ export default async function handler( sendResponse(rscResult, res); return; } + if (url.searchParams.has("__rsc")) { const response = new Response(rscResult.stream, { diff --git a/spiceflow/src/react/server-dom-client-optimized.tsx b/spiceflow/src/react/server-dom-client-optimized.tsx new file mode 100644 index 0000000..e5e4bbb --- /dev/null +++ b/spiceflow/src/react/server-dom-client-optimized.tsx @@ -0,0 +1,3 @@ + +import ReactServer from 'react-server-dom-vite/client' +export default ReactServer \ No newline at end of file diff --git a/spiceflow/src/react/types/ambient.d.ts b/spiceflow/src/react/types/ambient.d.ts index 75d9c2a..33e0c8b 100644 --- a/spiceflow/src/react/types/ambient.d.ts +++ b/spiceflow/src/react/types/ambient.d.ts @@ -22,7 +22,7 @@ declare module "react-server-dom-vite/server" { ): Promise; } -declare module "react-server-dom-vite/client" { +declare module "spiceflow/dist/react/server-dom-client-optimized" { export function createFromNodeStream( stream: import("node:stream").Readable, manifest: import(".").ClientReferenceManifest, diff --git a/spiceflow/src/spiceflow.ts b/spiceflow/src/spiceflow.ts index 8bc1120..4e24943 100644 --- a/spiceflow/src/spiceflow.ts +++ b/spiceflow/src/spiceflow.ts @@ -34,7 +34,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema' import { Context, MiddlewareContext } from './context.js' import { isProduction, ValidationError } from './error.js' import { isAsyncIterable, isResponse, redirect } from './utils.js' -import { json } from 'stream/consumers' +import { isValidElement } from 'react' const ajv = (addFormats.default || addFormats)( new (Ajv.default || Ajv)({ useDefaults: true }), @@ -71,6 +71,7 @@ export type InternalRoute = { validateBody?: ValidateFunction validateQuery?: ValidateFunction validateParams?: ValidateFunction + kind?: 'react' // prefix: string } @@ -675,6 +676,57 @@ export class Spiceflow< return this as any } + react< + const Path extends string, + const LocalSchema extends InputSchema, + const Schema extends UnwrapRoute, + const Handle extends InlineHandler< + Schema, + Singleton, + JoinPath + >, + >( + path: Path, + handler: Handle, + hook?: LocalHook< + LocalSchema, + Schema, + Singleton, + Definitions['error'], + Metadata['macro'], + JoinPath + >, + ): Spiceflow< + BasePath, + Scoped, + Singleton, + Definitions, + Metadata, + Routes & + CreateClient< + JoinPath, + { + get: { + body: Schema['body'] + params: undefined extends Schema['params'] + ? ResolvePath + : Schema['params'] + query: Schema['query'] + response: ComposeSpiceflowResponse + } + } + > + > { + this.add({ + method: 'GET', + path, + handler: handler, + hooks: hook, + kind: 'react' + }) + return this as any + } + private scoped?: Scoped = true as Scoped use( @@ -765,6 +817,12 @@ export class Spiceflow< params: _params, redirect, } satisfies MiddlewareContext + + if (route?.internalRoute?.kind === 'react') { + const root = await route.internalRoute?.handler(context) + console.log('root', root) + return root + } let handlerResponse: Response | undefined async function getResForError(err: any) { if (isResponse(err)) return err diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 727b9bc..68b1d83 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -30,7 +30,7 @@ export function spiceflowPlugin({ entry }): PluginOption { environments: { client: { optimizeDeps: { - include: ['react-dom/client', 'react-server-dom-vite/client'], + include: ['react-dom/client', 'spiceflow/dist/react/server-dom-client-optimized'], }, build: { manifest: true, @@ -262,7 +262,7 @@ export function spiceflowPlugin({ entry }): PluginOption { } else { const matches = code.matchAll(/export async function (\w+)\(/g) const result = [ - `import $$ReactClient from "react-server-dom-vite/client"`, + `import $$ReactClient from "spiceflow/dist/react/server-dom-client-optimized"`, ...[...matches].map( ([, name]) => `export const ${name} = $$ReactClient.createServerReference(${JSON.stringify(id + '#' + name)}, (...args) => __callServer(...args))`, diff --git a/spiceflow/tsconfig.json b/spiceflow/tsconfig.json index 90a6a11..9c18dab 100644 --- a/spiceflow/tsconfig.json +++ b/spiceflow/tsconfig.json @@ -2,7 +2,6 @@ "extends": "../tsconfig.base.json", "compilerOptions": { "rootDir": "src", - "typeRoots": ["./src/react/types/ambient.d.ts"], "module": "NodeNext", "target": "ESNext", "lib": ["DOM", "ESNext", "dom.iterable"], From fc86f4499a34caf2359d69a2ffb403ce84fccdd0 Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 12:28:01 +0100 Subject: [PATCH 009/100] fix, use parcel rsc-html-stream --- example-react/src/main.tsx | 9 ++++- pnpm-lock.yaml | 8 ++++ spiceflow/package.json | 1 + spiceflow/src/react/entry.client.tsx | 5 ++- spiceflow/src/react/entry.ssr.tsx | 7 ++-- spiceflow/src/react/utils/stream-script.ts | 45 ---------------------- spiceflow/src/spiceflow.ts | 8 +++- 7 files changed, 30 insertions(+), 53 deletions(-) delete mode 100644 spiceflow/src/react/utils/stream-script.ts diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 415df78..8f80651 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -1,10 +1,15 @@ import { Spiceflow } from 'spiceflow' import { IndexPage } from './app/index' +import { Layout } from './app/layout' const app = new Spiceflow() - .react('/', ({ request}) => { + .react('/', ({ request }) => { const url = new URL(request.url) - return + return ( + + + + ) }) .get('/hello', () => 'Hello, World!') .post('/echo', async ({ request }) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67cb941..fc2bd6f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,6 +188,9 @@ importers: react-server-dom-vite: specifier: npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14 version: '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14(react-dom@19.0.0(react@19.0.0))(react@19.0.0)' + rsc-html-stream: + specifier: ^0.0.4 + version: 0.0.4 spiceflow: specifier: '*' version: 1.6.1(@modelcontextprotocol/sdk@1.0.4) @@ -4751,6 +4754,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rsc-html-stream@0.0.4: + resolution: {integrity: sha512-1isiXIrlTI/vRLTvS3O4fMrO9qIHje1FSphufrIV5QfzHUgBDCZFwP9b8+rH63nbhxtcKTqfyziwM+2khfX0Uw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -10926,6 +10932,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.34.6 fsevents: 2.3.3 + rsc-html-stream@0.0.4: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 diff --git a/spiceflow/package.json b/spiceflow/package.json index b30ea42..3e0aa38 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -67,6 +67,7 @@ "react": "19.0.0", "react-dom": "19.0.0", "react-server-dom-vite": "npm:@jacob-ebey/react-server-dom-vite@19.0.0-experimental.14", + "rsc-html-stream": "^0.0.4", "spiceflow": "*", "superjson": "^2.2.2", "vite": "^6.1.0", diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index c453f2a..e412f8b 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -4,7 +4,8 @@ import ReactClient from "spiceflow/dist/react/server-dom-client-optimized"; import type { ServerPayload } from "./entry.rsc.js"; import type { CallServerFn } from "./types/index.js"; import { clientReferenceManifest } from "./utils/client-reference.js"; -import { getFlightStreamBrowser } from "./utils/stream-script.js"; +import {rscStream} from 'rsc-html-stream/client'; + async function main() { const callServer: CallServerFn = async (id, args) => { @@ -36,7 +37,7 @@ async function main() { const initialPayload = await ReactClient.createFromReadableStream( - getFlightStreamBrowser(), + rscStream, clientReferenceManifest, { callServer }, ); diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 5f90b85..d92d94b 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -10,7 +10,8 @@ import { fromWebToNodeReadable, sendResponse, } from "./utils/fetch.js"; -import { injectFlightStream } from "./utils/stream-script.js"; +import {injectRSCPayload} from 'rsc-html-stream/server'; + @@ -59,8 +60,8 @@ export default async function handler( const response = new Response( htmlStream - .pipeThrough(new TextDecoderStream()) - .pipeThrough(injectFlightStream(flightStream2)), + + .pipeThrough(injectRSCPayload(flightStream2)), { headers: { "content-type": "text/html;charset=utf-8", diff --git a/spiceflow/src/react/utils/stream-script.ts b/spiceflow/src/react/utils/stream-script.ts deleted file mode 100644 index cedea64..0000000 --- a/spiceflow/src/react/utils/stream-script.ts +++ /dev/null @@ -1,45 +0,0 @@ -const INIT_SCRIPT = ` -self.__flightStream = new ReadableStream({ - start(controller) { - self.__f_push = (c) => controller.enqueue(c); - self.__f_close = () => controller.close(); - } -}).pipeThrough(new TextEncoderStream()); -`; - -export function injectFlightStream(stream: ReadableStream) { - return new TransformStream({ - async transform(chunk, controller) { - // TODO: chunk is not guaranteed to include entire end tag `` - if (chunk.includes("")) { - chunk = chunk.replace( - "", - () => ``, - ); - } - if (chunk.includes("")) { - const i = chunk.indexOf(""); - controller.enqueue(chunk.slice(0, i)); - await stream.pipeThrough(new TextDecoderStream()).pipeTo( - new WritableStream({ - write(chunk) { - controller.enqueue( - ``, - ); - }, - close() { - controller.enqueue(``); - }, - }), - ); - controller.enqueue(chunk.slice(i)); - } else { - controller.enqueue(chunk); - } - }, - }); -} - -export function getFlightStreamBrowser(): ReadableStream { - return (self as any).__flightStream; -} diff --git a/spiceflow/src/spiceflow.ts b/spiceflow/src/spiceflow.ts index 4e24943..63334cd 100644 --- a/spiceflow/src/spiceflow.ts +++ b/spiceflow/src/spiceflow.ts @@ -724,6 +724,13 @@ export class Spiceflow< hooks: hook, kind: 'react' }) + this.add({ + method: 'POST', + path, + handler: handler, + hooks: hook, + kind: 'react' + }) return this as any } @@ -820,7 +827,6 @@ export class Spiceflow< if (route?.internalRoute?.kind === 'react') { const root = await route.internalRoute?.handler(context) - console.log('root', root) return root } let handlerResponse: Response | undefined From 66808b97b100b349dc35d2216a2524e5e5f4d2ad Mon Sep 17 00:00:00 2001 From: remorses Date: Sun, 9 Feb 2025 14:46:41 +0100 Subject: [PATCH 010/100] adding e2e tests --- example-react/.gitignore | 4 + example-react/e2e/basic.test.ts | 128 ++++++++++++++++++ example-react/e2e/helper.ts | 15 ++ example-react/package.json | 6 +- example-react/playwright.config.ts | 32 +++++ example-react/src/app/client.tsx | 4 + example-react/src/app/index.tsx | 2 +- example-react/src/main.tsx | 10 +- example-react/tsconfig.json | 44 +++--- pnpm-lock.yaml | 38 ++++++ spiceflow/src/react/utils/client-reference.ts | 1 + spiceflow/src/spiceflow.ts | 38 ++---- spiceflow/src/vite.tsx | 1 + 13 files changed, 268 insertions(+), 55 deletions(-) create mode 100644 example-react/.gitignore create mode 100644 example-react/e2e/basic.test.ts create mode 100644 example-react/e2e/helper.ts create mode 100644 example-react/playwright.config.ts diff --git a/example-react/.gitignore b/example-react/.gitignore new file mode 100644 index 0000000..beed9cf --- /dev/null +++ b/example-react/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +test-results +*.tsbuildinfo diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts new file mode 100644 index 0000000..8a0b083 --- /dev/null +++ b/example-react/e2e/basic.test.ts @@ -0,0 +1,128 @@ +import { type Page, expect, test } from "@playwright/test"; +import { createEditor } from "./helper.js"; + +test("client reference", async ({ page }) => { + await page.goto("/"); + await page.getByText("[hydrated: 1]").click(); + await page.getByText("Client counter: 0").click(); + await page + .getByTestId("client-counter") + .getByRole("button", { name: "+" }) + .click(); + await page.getByText("Client counter: 1").click(); + await page.reload(); + await page.getByText("Client counter: 0").click(); +}); + +test("server reference in server @js", async ({ page }) => { + await testServerAction(page); +}); + +test.describe(() => { + test.use({ javaScriptEnabled: false }); + test("server reference in server @nojs", async ({ page }) => { + await testServerAction(page); + }); +}); + +async function testServerAction(page: Page) { + await page.goto("/"); + await page.getByText("Server counter: 0").click(); + await page + .getByTestId("server-counter") + .getByRole("button", { name: "+" }) + .click(); + await page.getByText("Server counter: 1").click(); + await page.goto("/"); + await page.getByText("Server counter: 1").click(); + await page + .getByTestId("server-counter") + .getByRole("button", { name: "-" }) + .click(); + await page.getByText("Server counter: 0").click(); +} + +test("server reference in client @js", async ({ page }) => { + await testServerAction2(page, { js: true }); +}); + +test.describe(() => { + test.use({ javaScriptEnabled: false }); + test("server reference in client @nojs", async ({ page }) => { + await testServerAction2(page, { js: false }); + }); +}); + +async function testServerAction2(page: Page, options: { js: boolean }) { + await page.goto("/"); + if (options.js) { + await page.getByText("[hydrated: 1]").click(); + } + await page.locator('input[name="x"]').fill("2"); + await page.locator('input[name="y"]').fill("3"); + await page.locator('input[name="y"]').press("Enter"); + await expect(page.getByTestId("calculator-answer")).toContainText("5"); + await page.locator('input[name="x"]').fill("2"); + await page.locator('input[name="y"]').fill("three"); + await page.locator('input[name="y"]').press("Enter"); + await expect(page.getByTestId("calculator-answer")).toContainText( + "(invalid input)", + ); + if (options.js) { + await expect(page.locator('input[name="x"]')).toHaveValue("2"); + await expect(page.locator('input[name="y"]')).toHaveValue("three"); + } else { + await expect(page.locator('input[name="x"]')).toHaveValue(""); + await expect(page.locator('input[name="y"]')).toHaveValue(""); + } +} + +test("client hmr @dev", async ({ page }) => { + await page.goto("/"); + await page.getByText("[hydrated: 1]").click(); + // client +1 + await page.getByText("Client counter: 0").click(); + await page + .getByTestId("client-counter") + .getByRole("button", { name: "+" }) + .click(); + await page.getByText("Client counter: 1").click(); + // edit client + using file = createEditor("src/app/client.tsx"); + file.edit((s) => s.replace("Client counter", "Client [EDIT] counter")); + await page.getByText("Client [EDIT] counter: 1").click(); +}); + +test("server hmr @dev", async ({ page }) => { + await page.goto("/"); + await page.getByText("[hydrated: 1]").click(); + + // server +1 + await page.getByText("Server counter: 0").click(); + await page + .getByTestId("server-counter") + .getByRole("button", { name: "+" }) + .click(); + await page.getByText("Server counter: 1").click(); + + // client +1 + await page.getByText("Client counter: 0").click(); + await page + .getByTestId("client-counter") + .getByRole("button", { name: "+" }) + .click(); + await page.getByText("Client counter: 1").click(); + + // edit server + using file = createEditor("src/app/index.tsx"); + file.edit((s) => s.replace("Server counter", "Server [EDIT] counter")); + await page.getByText("Server [EDIT] counter: 1").click(); + await page.getByText("Client counter: 1").click(); + + // server -1 + await page + .getByTestId("server-counter") + .getByRole("button", { name: "-" }) + .click(); + await page.getByText("Server [EDIT] counter: 0").click(); +}); diff --git a/example-react/e2e/helper.ts b/example-react/e2e/helper.ts new file mode 100644 index 0000000..cc33429 --- /dev/null +++ b/example-react/e2e/helper.ts @@ -0,0 +1,15 @@ +import fs from "node:fs"; + +export function createEditor(filepath: string) { + let init = fs.readFileSync(filepath, "utf-8"); + let data = init; + return { + edit(editFn: (data: string) => string) { + data = editFn(data); + fs.writeFileSync(filepath, data); + }, + [Symbol.dispose]() { + fs.writeFileSync(filepath, init); + }, + }; +} diff --git a/example-react/package.json b/example-react/package.json index 802b8dd..a43f241 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -4,11 +4,15 @@ "description": "", "main": "index.js", "scripts": { - "dev": "vite" + "dev": "vite", + "preview": "vite preview", + "test-e2e": "playwright test", + "test-e2e-preview": "E2E_PREVIEW=1 playwright test" }, "keywords": [], "author": "remorses ", "dependencies": { + "@playwright/test": "^1.50.1", "react": "19.0.0", "react-dom": "19.0.0", "spiceflow": "workspace:*", diff --git a/example-react/playwright.config.ts b/example-react/playwright.config.ts new file mode 100644 index 0000000..3dab541 --- /dev/null +++ b/example-react/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig, devices } from "@playwright/test"; + +const port = Number(process.env.E2E_PORT || 6174); +const isPreview = Boolean(process.env.E2E_PREVIEW); +const command = isPreview + ? `pnpm preview --port ${port} --strict-port` + : `pnpm dev --port ${port} --strict-port`; + +export default defineConfig({ + testDir: "e2e", + use: { + trace: "on-first-retry", + }, + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + viewport: null, + deviceScaleFactor: undefined, + }, + }, + ], + webServer: { + command, + port, + }, + grepInvert: isPreview ? /@dev/ : /@build/, + forbidOnly: !!process.env["CI"], + retries: process.env["CI"] ? 2 : 0, + reporter: "list", +}); diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index 5f63437..cbc9732 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -60,3 +60,7 @@ export function Calculator() { ); } + + + + diff --git a/example-react/src/app/index.tsx b/example-react/src/app/index.tsx index 268655d..648f2a0 100644 --- a/example-react/src/app/index.tsx +++ b/example-react/src/app/index.tsx @@ -12,7 +12,7 @@ export async function IndexPage() { data-testid="server-counter" style={{ padding: "0.5rem" }} > -
Server counter: {getCounter()}
+
Server [EDIT] counter: {getCounter()}
+ ); } - - - diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index ffeba74..f73cb49 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -10,14 +10,7 @@ const app = new Spiceflow() const url = new URL(request.url); return ; }) - .page("/:id", async ({ request, params }) => { - const url = new URL(request.url); - return ( - - - - ); - }) + .get("/hello", () => "Hello, World!") .page("/redirect", async () => { throw new Response("Redirect", { @@ -27,6 +20,14 @@ const app = new Spiceflow() }, }); }) + .page("/:id", async ({ request, params }) => { + const url = new URL(request.url); + return ( + + + + ); + }) .page("/redirect-in-rsc", async () => { return ; }) From c1c3445b3e194f2a2f7237edad1008b4b1a5d153 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 06:48:03 +0100 Subject: [PATCH 033/100] layouts seem to work too --- example-react/src/main.tsx | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index f73cb49..dba4af3 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -20,12 +20,25 @@ const app = new Spiceflow() }, }); }) + .layout("/page/*", async ({ request, children }) => { + return ( +
+

/page layout

+ {children} +
+ ); + }) + .page("/page", async ({ request }) => { + const url = new URL(request.url); + return ; + }) .page("/:id", async ({ request, params }) => { const url = new URL(request.url); return ( - +
+

:id page

- +
); }) .page("/redirect-in-rsc", async () => { From 6bf8bf1d1f09d7e391f5459c1be2473afb493457 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 06:48:21 +0100 Subject: [PATCH 034/100] more layouts --- example-react/src/main.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index dba4af3..296d5d8 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -23,7 +23,15 @@ const app = new Spiceflow() .layout("/page/*", async ({ request, children }) => { return (
-

/page layout

+

/page layout 1

+ {children} +
+ ); + }) + .layout("/page/*", async ({ request, children }) => { + return ( +
+

/page layout 2

{children}
); From d70da5e997b386fa02c986d88ce0ab666d2b4110 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 06:49:20 +0100 Subject: [PATCH 035/100] tried links too --- example-react/src/main.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 296d5d8..1effc5c 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -38,7 +38,21 @@ const app = new Spiceflow() }) .page("/page", async ({ request }) => { const url = new URL(request.url); - return ; + return ( +
+ ); + }) + .page("/page/1", async ({ request }) => { + const url = new URL(request.url); + return ( +
+ /page + +
+ ); }) .page("/:id", async ({ request, params }) => { const url = new URL(request.url); From 75f3443d7ef11c89549313ef24e181c304f0b09c Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 10:39:20 +0100 Subject: [PATCH 036/100] tailwindcss works --- example-react/package.json | 4 +- example-react/src/main.tsx | 1 + example-react/src/styles.css | 1 + example-react/vite.config.ts | 3 + pnpm-lock.yaml | 473 +++++++++++++++++++++++++++-------- 5 files changed, 382 insertions(+), 100 deletions(-) create mode 100644 example-react/src/styles.css diff --git a/example-react/package.json b/example-react/package.json index a2a8a6f..8dd26f9 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "index.js", + "type": "module", "scripts": { "dev": "vite", "preview": "vite preview", @@ -13,10 +14,11 @@ "author": "remorses ", "dependencies": { "@playwright/test": "^1.50.1", + "@tailwindcss/vite": "^4.0.5", "react": "19.0.0", "react-dom": "19.0.0", "spiceflow": "workspace:*", - + "tailwindcss": "^4.0.5", "vite": "^6.1.0" }, "license": "ISC", diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 1effc5c..c4bca19 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -1,6 +1,7 @@ import { Spiceflow } from "spiceflow"; import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; +import './styles.css' const app = new Spiceflow() .layout("/*", async ({ children, request }) => { diff --git a/example-react/src/styles.css b/example-react/src/styles.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/example-react/src/styles.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/example-react/vite.config.ts b/example-react/vite.config.ts index 0205758..4fa4a4d 100644 --- a/example-react/vite.config.ts +++ b/example-react/vite.config.ts @@ -1,11 +1,14 @@ import { defineConfig } from 'vite' import { spiceflowPlugin } from 'spiceflow/dist/vite' +import tailwindcss from '@tailwindcss/vite' + import inspect from 'vite-plugin-inspect' export default defineConfig({ clearScreen: false, plugins: [ // inspect(), + tailwindcss(), spiceflowPlugin({ entry: './src/main.tsx', }), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1b8cf7f..6010f2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,16 +32,19 @@ importers: version: 5.7.3 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vitest: specifier: ^3.0.4 - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) example-react: dependencies: '@playwright/test': specifier: ^1.50.1 version: 1.50.1 + '@tailwindcss/vite': + specifier: ^4.0.5 + version: 4.0.5(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) react: specifier: 19.0.0 version: 19.0.0 @@ -51,13 +54,16 @@ importers: spiceflow: specifier: workspace:* version: link:../spiceflow + tailwindcss: + specifier: ^4.0.5 + version: 4.0.5 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) devDependencies: vite-plugin-inspect: specifier: ^10.1.1 - version: 10.1.1(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 10.1.1(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) openapi-schema-diff: dependencies: @@ -76,10 +82,10 @@ importers: version: 10.1.3 eslint: specifier: ^9.19.0 - version: 9.19.0(jiti@1.21.7) + version: 9.19.0(jiti@2.4.2) neostandard: specifier: ^0.12.0 - version: 0.12.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + version: 0.12.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) typescript: specifier: ^5.7.3 version: 5.7.3 @@ -164,7 +170,7 @@ importers: version: 0.0.0 '@jacob-ebey/vite-react-server-dom': specifier: ^0.0.12 - version: 0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@modelcontextprotocol/sdk': specifier: ^1.0.4 version: 1.0.4 @@ -173,7 +179,7 @@ importers: version: 0.34.15 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -215,7 +221,7 @@ importers: version: 0.0.11(rollup@4.34.6) vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) zod: specifier: ^3.24.1 version: 3.24.1 @@ -307,7 +313,7 @@ importers: version: 4.20250129.0 '@remix-run/dev': specifier: ^2.15.3 - version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) + version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -334,10 +340,10 @@ importers: version: 5.7.3 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) wrangler: specifier: ^3.48.0 version: 3.107.2(@cloudflare/workers-types@4.20250129.0) @@ -1936,11 +1942,89 @@ packages: peerDependencies: eslint: '>=8.40.0' + '@tailwindcss/node@4.0.5': + resolution: {integrity: sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog==} + + '@tailwindcss/oxide-android-arm64@4.0.5': + resolution: {integrity: sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.0.5': + resolution: {integrity: sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.0.5': + resolution: {integrity: sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.0.5': + resolution: {integrity: sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5': + resolution: {integrity: sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.0.5': + resolution: {integrity: sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.0.5': + resolution: {integrity: sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.0.5': + resolution: {integrity: sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.0.5': + resolution: {integrity: sha512-z2RzUvOQl0ZqrZqmCFP53tJbBXQ3UmLD/E6J7+q0e+4VaFnXCcIYTfQbHgI8f3fash+q6gK80Ko/ywEQ+bvv6Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-win32-arm64-msvc@4.0.5': + resolution: {integrity: sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.0.5': + resolution: {integrity: sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.0.5': + resolution: {integrity: sha512-iWGyOCu0TuzvCBisWbGv2K9+7QCfE0ztgtrZOvb9iF7V7ChVkD15Obe3HevZrhjngAc34jDA+OMSuSvkrpTy4A==} + engines: {node: '>= 10'} + '@tailwindcss/typography@0.5.16': resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.0.5': + resolution: {integrity: sha512-/i4hjLTUYVjUG0MTUviQP3HR/hzwyzv8Sq4sz2pnsNuf+FIjjhJB0vcnIMH1KIX0k8ozD6CBv2Dl76tlm/JFFA==} + peerDependencies: + vite: ^5.2.0 || ^6 + '@tsconfig/node10@1.0.11': resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} @@ -2701,6 +2785,11 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3667,6 +3756,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + js-base64@3.7.7: resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} @@ -3739,6 +3832,70 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lightningcss-darwin-arm64@1.29.1: + resolution: {integrity: sha512-HtR5XJ5A0lvCqYAoSv2QdZZyoHNttBpa5EP9aNuzBQeKGfbyH5+UipLWvVzpP4Uml5ej4BYs5I9Lco9u1fECqw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.1: + resolution: {integrity: sha512-k33G9IzKUpHy/J/3+9MCO4e+PzaFblsgBjSGlpAaFikeBFm8B/CkO3cKU9oI4g+fjS2KlkLM/Bza9K/aw8wsNA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.1: + resolution: {integrity: sha512-0SUW22fv/8kln2LnIdOCmSuXnxgxVC276W5KLTwoehiO0hxkacBxjHOL5EtHD8BAXg2BvuhsJPmVMasvby3LiQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.1: + resolution: {integrity: sha512-sD32pFvlR0kDlqsOZmYqH/68SqUMPNj+0pucGxToXZi4XZgZmqeX/NkxNKCPsswAXU3UeYgDSpGhu05eAufjDg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.1: + resolution: {integrity: sha512-0+vClRIZ6mmJl/dxGuRsE197o1HDEeeRk6nzycSy2GofC2JsY4ifCRnvUWf/CUBQmlrvMzt6SMQNMSEu22csWQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.1: + resolution: {integrity: sha512-UKMFrG4rL/uHNgelBsDwJcBqVpzNJbzsKkbI3Ja5fg00sgQnHw/VrzUTEc4jhZ+AN2BvQYz/tkHu4vt1kLuJyw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.1: + resolution: {integrity: sha512-u1S+xdODy/eEtjADqirA774y3jLcm8RPtYztwReEXoZKdzgsHYPl0s5V52Tst+GKzqjebkULT86XMSxejzfISw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.1: + resolution: {integrity: sha512-L0Tx0DtaNUTzXv0lbGCLB/c/qEADanHbu4QdcNOXLIe1i8i22rZRpbT3gpWYsCh9aSL9zFujY/WmEXIatWvXbw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.1: + resolution: {integrity: sha512-QoOVnkIEFfbW4xPi+dpdft/zAKmgLgsRHfJalEPYuJDOWf7cLQzYg0DEh8/sn737FaeMJxHZRc1oBreiwZCjog==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.29.1: + resolution: {integrity: sha512-NygcbThNBe4JElP+olyTI/doBNGJvLs3bFCRPdvuCcxZCcCZ71B858IHpdm7L1btZex0FvCmM17FK98Y9MRy1Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.29.1: + resolution: {integrity: sha512-FmGoeD4S05ewj+AkhTY+D+myDvXI6eL27FjHIjoyUkO/uw7WZD1fBVs0QxeYWa7E17CUHJaYX/RUGISCtcrG4Q==} + engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -5221,6 +5378,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + tailwindcss@4.0.5: + resolution: {integrity: sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg==} + tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} @@ -6763,9 +6923,9 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0(jiti@1.21.7))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0(jiti@2.4.2))': dependencies: - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -6855,13 +7015,13 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@jacob-ebey/vite-react-server-dom@0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@jacob-ebey/vite-react-server-dom@0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@jacob-ebey/react-server-dom-vite': 19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mjackson/node-fetch-server': 0.5.0 - '@vitejs/plugin-react': 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@vitejs/plugin-react': 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) unplugin-rsc: 0.0.11(rollup@4.34.6) - vite: 6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - rollup - supports-color @@ -7080,7 +7240,7 @@ snapshots: optionalDependencies: typescript: 5.7.3 - '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': + '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': dependencies: '@babel/core': 7.26.7 '@babel/generator': 7.26.5 @@ -7097,7 +7257,7 @@ snapshots: '@remix-run/router': 1.22.0 '@remix-run/server-runtime': 2.15.3(typescript@5.7.3) '@types/mdx': 2.0.13 - '@vanilla-extract/integration': 6.5.0(@types/node@22.13.0)(terser@5.31.6) + '@vanilla-extract/integration': 6.5.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 @@ -7136,11 +7296,11 @@ snapshots: tar-fs: 2.1.2 tsconfig-paths: 4.2.0 valibot: 0.41.0(typescript@5.7.3) - vite-node: 1.6.0(@types/node@22.13.0)(terser@5.31.6) + vite-node: 1.6.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) ws: 7.5.10 optionalDependencies: typescript: 5.7.3 - vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) wrangler: 3.107.2(@cloudflare/workers-types@4.20250129.0) transitivePeerDependencies: - '@types/node' @@ -7364,10 +7524,10 @@ snapshots: hast-util-to-string: 2.0.0 unist-util-visit: 4.1.2 - '@stylistic/eslint-plugin@2.11.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3)': + '@stylistic/eslint-plugin@2.11.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - eslint: 9.19.0(jiti@1.21.7) + '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.4.2) eslint-visitor-keys: 4.2.0 espree: 10.3.0 estraverse: 5.3.0 @@ -7376,6 +7536,59 @@ snapshots: - supports-color - typescript + '@tailwindcss/node@4.0.5': + dependencies: + enhanced-resolve: 5.18.0 + jiti: 2.4.2 + tailwindcss: 4.0.5 + + '@tailwindcss/oxide-android-arm64@4.0.5': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.0.5': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.0.5': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.0.5': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.0.5': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.0.5': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.0.5': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.0.5': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.0.5': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.0.5': + optional: true + + '@tailwindcss/oxide@4.0.5': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.0.5 + '@tailwindcss/oxide-darwin-arm64': 4.0.5 + '@tailwindcss/oxide-darwin-x64': 4.0.5 + '@tailwindcss/oxide-freebsd-x64': 4.0.5 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.5 + '@tailwindcss/oxide-linux-arm64-gnu': 4.0.5 + '@tailwindcss/oxide-linux-arm64-musl': 4.0.5 + '@tailwindcss/oxide-linux-x64-gnu': 4.0.5 + '@tailwindcss/oxide-linux-x64-musl': 4.0.5 + '@tailwindcss/oxide-win32-arm64-msvc': 4.0.5 + '@tailwindcss/oxide-win32-x64-msvc': 4.0.5 + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)))': dependencies: lodash.castarray: 4.4.0 @@ -7384,6 +7597,14 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@tailwindcss/vite@4.0.5(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + dependencies: + '@tailwindcss/node': 4.0.5 + '@tailwindcss/oxide': 4.0.5 + lightningcss: 1.29.1 + tailwindcss: 4.0.5 + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + '@tsconfig/node10@1.0.11': {} '@tsconfig/node12@1.0.11': {} @@ -7491,15 +7712,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.19.0 - '@typescript-eslint/type-utils': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.19.0 - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -7508,14 +7729,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.19.0 '@typescript-eslint/types': 8.19.0 '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.19.0 debug: 4.4.0 - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -7525,12 +7746,12 @@ snapshots: '@typescript-eslint/types': 8.19.0 '@typescript-eslint/visitor-keys': 8.19.0 - '@typescript-eslint/type-utils@8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) ts-api-utils: 1.4.3(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: @@ -7552,13 +7773,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/utils@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.19.0 '@typescript-eslint/types': 8.19.0 '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.3) - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -7593,7 +7814,7 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@vanilla-extract/integration@6.5.0(@types/node@22.13.0)(terser@5.31.6)': + '@vanilla-extract/integration@6.5.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6)': dependencies: '@babel/core': 7.26.7 '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.7) @@ -7606,8 +7827,8 @@ snapshots: lodash: 4.17.21 mlly: 1.7.4 outdent: 0.8.0 - vite: 5.4.14(@types/node@22.13.0)(terser@5.31.6) - vite-node: 1.6.0(@types/node@22.13.0)(terser@5.31.6) + vite: 5.4.14(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) + vite-node: 1.6.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7622,14 +7843,14 @@ snapshots: '@vanilla-extract/private@1.0.6': {} - '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.7 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.7) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -7640,13 +7861,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@vitest/mocker@3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) '@vitest/pretty-format@3.0.4': dependencies: @@ -8214,6 +8435,8 @@ snapshots: detect-indent@6.1.0: {} + detect-libc@1.0.3: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -8549,9 +8772,9 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.19.0(jiti@1.21.7)): + eslint-compat-utils@0.5.1(eslint@9.19.0(jiti@2.4.2)): dependencies: - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) semver: 7.7.0 eslint-import-resolver-node@0.3.9: @@ -8562,38 +8785,38 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.19.0(jiti@1.21.7)): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 enhanced-resolve: 5.18.0 - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.3.0 is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@9.19.0(jiti@1.21.7)): + eslint-plugin-es-x@7.8.0(eslint@9.19.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - eslint: 9.19.0(jiti@1.21.7) - eslint-compat-utils: 0.5.1(eslint@9.19.0(jiti@1.21.7)) + eslint: 9.19.0(jiti@2.4.2) + eslint-compat-utils: 0.5.1(eslint@9.19.0(jiti@2.4.2)) - eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3): + eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3): dependencies: '@types/doctrine': 0.0.9 '@typescript-eslint/scope-manager': 8.19.0 - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 doctrine: 3.0.0 enhanced-resolve: 5.18.0 - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 get-tsconfig: 4.8.1 is-glob: 4.0.3 @@ -8605,24 +8828,24 @@ snapshots: - supports-color - typescript - eslint-plugin-n@17.15.1(eslint@9.19.0(jiti@1.21.7)): + eslint-plugin-n@17.15.1(eslint@9.19.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) enhanced-resolve: 5.18.0 - eslint: 9.19.0(jiti@1.21.7) - eslint-plugin-es-x: 7.8.0(eslint@9.19.0(jiti@1.21.7)) + eslint: 9.19.0(jiti@2.4.2) + eslint-plugin-es-x: 7.8.0(eslint@9.19.0(jiti@2.4.2)) get-tsconfig: 4.8.1 globals: 15.14.0 ignore: 5.3.2 minimatch: 9.0.5 semver: 7.6.3 - eslint-plugin-promise@7.2.1(eslint@9.19.0(jiti@1.21.7)): + eslint-plugin-promise@7.2.1(eslint@9.19.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@1.21.7)) - eslint: 9.19.0(jiti@1.21.7) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) + eslint: 9.19.0(jiti@2.4.2) - eslint-plugin-react@7.37.3(eslint@9.19.0(jiti@1.21.7)): + eslint-plugin-react@7.37.3(eslint@9.19.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -8630,7 +8853,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.19.0(jiti@1.21.7) + eslint: 9.19.0(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -8653,9 +8876,9 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.19.0(jiti@1.21.7): + eslint@9.19.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.1 '@eslint/core': 0.10.0 @@ -8690,7 +8913,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.4.2 transitivePeerDependencies: - supports-color @@ -9515,6 +9738,8 @@ snapshots: jiti@1.21.7: {} + jiti@2.4.2: {} + js-base64@3.7.7: {} js-tokens@4.0.0: {} @@ -9582,6 +9807,51 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lightningcss-darwin-arm64@1.29.1: + optional: true + + lightningcss-darwin-x64@1.29.1: + optional: true + + lightningcss-freebsd-x64@1.29.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.29.1: + optional: true + + lightningcss-linux-arm64-gnu@1.29.1: + optional: true + + lightningcss-linux-arm64-musl@1.29.1: + optional: true + + lightningcss-linux-x64-gnu@1.29.1: + optional: true + + lightningcss-linux-x64-musl@1.29.1: + optional: true + + lightningcss-win32-arm64-msvc@1.29.1: + optional: true + + lightningcss-win32-x64-msvc@1.29.1: + optional: true + + lightningcss@1.29.1: + dependencies: + detect-libc: 1.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.1 + lightningcss-darwin-x64: 1.29.1 + lightningcss-freebsd-x64: 1.29.1 + lightningcss-linux-arm-gnueabihf: 1.29.1 + lightningcss-linux-arm64-gnu: 1.29.1 + lightningcss-linux-arm64-musl: 1.29.1 + lightningcss-linux-x64-gnu: 1.29.1 + lightningcss-linux-x64-musl: 1.29.1 + lightningcss-win32-arm64-msvc: 1.29.1 + lightningcss-win32-x64-msvc: 1.29.1 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -10427,20 +10697,20 @@ snapshots: negotiator@0.6.3: {} - neostandard@0.12.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3): + neostandard@0.12.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3): dependencies: '@humanwhocodes/gitignore-to-minimatch': 1.0.2 - '@stylistic/eslint-plugin': 2.11.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - eslint: 9.19.0(jiti@1.21.7) - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.19.0(jiti@1.21.7)) - eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - eslint-plugin-n: 17.15.1(eslint@9.19.0(jiti@1.21.7)) - eslint-plugin-promise: 7.2.1(eslint@9.19.0(jiti@1.21.7)) - eslint-plugin-react: 7.37.3(eslint@9.19.0(jiti@1.21.7)) + '@stylistic/eslint-plugin': 2.11.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.4.2) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2)) + eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + eslint-plugin-n: 17.15.1(eslint@9.19.0(jiti@2.4.2)) + eslint-plugin-promise: 7.2.1(eslint@9.19.0(jiti@2.4.2)) + eslint-plugin-react: 7.37.3(eslint@9.19.0(jiti@2.4.2)) find-up: 5.0.0 globals: 15.14.0 peowly: 1.3.2 - typescript-eslint: 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) + typescript-eslint: 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) transitivePeerDependencies: - eslint-plugin-import - supports-color @@ -11669,6 +11939,8 @@ snapshots: transitivePeerDependencies: - ts-node + tailwindcss@4.0.5: {} + tapable@2.2.1: {} tar-fs@2.1.2: @@ -11864,12 +12136,12 @@ snapshots: possible-typed-array-names: 1.0.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3): + typescript-eslint@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/parser': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@1.21.7))(typescript@5.7.3) - eslint: 9.19.0(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.19.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -12111,13 +12383,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@1.6.0(@types/node@22.13.0)(terser@5.31.6): + vite-node@1.6.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6): dependencies: cac: 6.7.14 debug: 4.4.0 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.14(@types/node@22.13.0)(terser@5.31.6) + vite: 5.4.14(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) transitivePeerDependencies: - '@types/node' - less @@ -12129,13 +12401,13 @@ snapshots: - supports-color - terser - vite-node@3.0.4(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vite-node@3.0.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.2 - vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -12150,29 +12422,29 @@ snapshots: - tsx - yaml - vite-plugin-inspect@10.1.1(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): + vite-plugin-inspect@10.1.1(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): dependencies: debug: 4.4.0 error-stack-parser-es: 1.0.5 open: 10.1.0 picocolors: 1.1.1 sirv: 3.0.0 - vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color - vite-tsconfig-paths@4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): + vite-tsconfig-paths@4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.3) optionalDependencies: - vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.14(@types/node@22.13.0)(terser@5.31.6): + vite@5.4.14(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6): dependencies: esbuild: 0.21.5 postcss: 8.5.1 @@ -12180,9 +12452,10 @@ snapshots: optionalDependencies: '@types/node': 22.13.0 fsevents: 2.3.3 + lightningcss: 1.29.1 terser: 5.31.6 - vite@6.1.0(@types/node@22.12.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 postcss: 8.5.1 @@ -12190,12 +12463,13 @@ snapshots: optionalDependencies: '@types/node': 22.12.0 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.4.2 + lightningcss: 1.29.1 terser: 5.31.6 tsx: 4.19.2 yaml: 2.7.0 - vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 postcss: 8.5.1 @@ -12203,15 +12477,16 @@ snapshots: optionalDependencies: '@types/node': 22.13.0 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.4.2 + lightningcss: 1.29.1 terser: 5.31.6 tsx: 4.19.2 yaml: 2.7.0 - vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.4 - '@vitest/mocker': 3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/mocker': 3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.4 '@vitest/runner': 3.0.4 '@vitest/snapshot': 3.0.4 @@ -12227,8 +12502,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.1.0(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) - vite-node: 3.0.4(@types/node@22.13.0)(jiti@1.21.7)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite-node: 3.0.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 From df79c2450e323400e6f7269397653df18fc16d27 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 12:33:48 +0100 Subject: [PATCH 037/100] fix build time css, debugging missing server references --- example-react/package.json | 1 + example-react/server.js | 12 ++ example-react/src/app/button.tsx | 17 ++ example-react/src/app/client.css | 5 + example-react/src/app/client.tsx | 1 + example-react/src/app/index.tsx | 11 +- example-react/src/main.tsx | 3 +- example-react/vite.config.ts | 26 +-- openapi-schema-diff | 2 +- pnpm-lock.yaml | 167 ++++++++---------- sdk/package.json | 2 +- spiceflow/package.json | 2 +- spiceflow/src/react/components.tsx | 1 - spiceflow/src/react/css.tsx | 28 +++ spiceflow/src/react/entry.rsc.tsx | 7 +- spiceflow/src/react/entry.ssr.tsx | 11 +- spiceflow/src/react/utils/client-reference.ts | 6 +- spiceflow/src/spiceflow.ts | 1 - spiceflow/src/vite.tsx | 82 ++++++--- 19 files changed, 234 insertions(+), 151 deletions(-) create mode 100644 example-react/server.js create mode 100644 example-react/src/app/button.tsx create mode 100644 example-react/src/app/client.css create mode 100644 spiceflow/src/react/css.tsx diff --git a/example-react/package.json b/example-react/package.json index 8dd26f9..b234960 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "dev": "vite", + "build": "vite build --app", "preview": "vite preview", "test-e2e": "playwright test", "test-e2e-preview": "E2E_PREVIEW=1 playwright test" diff --git a/example-react/server.js b/example-react/server.js new file mode 100644 index 0000000..f5a7ce4 --- /dev/null +++ b/example-react/server.js @@ -0,0 +1,12 @@ +import handler from './dist/ssr/index.js' + +import http from 'node:http' + +const server = http.createServer((req, res) => { + handler(req, res) +}) + +const port = process.env.PORT || 3000 +server.listen(port, () => { + console.log(`Server running at http://localhost:${port}`) +}) diff --git a/example-react/src/app/button.tsx b/example-react/src/app/button.tsx new file mode 100644 index 0000000..db7af13 --- /dev/null +++ b/example-react/src/app/button.tsx @@ -0,0 +1,17 @@ +"use client"; + +import React from "react"; + +type ButtonProps = React.ButtonHTMLAttributes; + +export function Button(props: ButtonProps) { + const className = `px-2 py-1 rounded-md transition-colors ${props.className || ""}`; + + return ( + - + + diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index c4bca19..f94b75c 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -3,13 +3,14 @@ import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; import './styles.css' + const app = new Spiceflow() .layout("/*", async ({ children, request }) => { return {children}; }) .page("/", async ({ request }) => { const url = new URL(request.url); - return ; + return ; }) .get("/hello", () => "Hello, World!") diff --git a/example-react/vite.config.ts b/example-react/vite.config.ts index 4fa4a4d..fa71a7b 100644 --- a/example-react/vite.config.ts +++ b/example-react/vite.config.ts @@ -1,16 +1,16 @@ -import { defineConfig } from 'vite' -import { spiceflowPlugin } from 'spiceflow/dist/vite' -import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from "vite"; +import { spiceflowPlugin } from "spiceflow/dist/vite"; +import tailwindcss from "@tailwindcss/vite"; -import inspect from 'vite-plugin-inspect' +import inspect from "vite-plugin-inspect"; export default defineConfig({ - clearScreen: false, - plugins: [ - // inspect(), - tailwindcss(), - spiceflowPlugin({ - entry: './src/main.tsx', - }), - ], -}) + clearScreen: false, + plugins: [ + // inspect(), + tailwindcss(), + spiceflowPlugin({ + entry: "./src/main.tsx", + }), + ], +}); diff --git a/openapi-schema-diff b/openapi-schema-diff index e19fb8d..1fabd44 160000 --- a/openapi-schema-diff +++ b/openapi-schema-diff @@ -1 +1 @@ -Subproject commit e19fb8d331e11235801d12d2168b93d495e086d2 +Subproject commit 1fabd44001a2f2c1464342c009bf60e42574fe64 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6010f2a..78bd58c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,10 +32,10 @@ importers: version: 5.7.3 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vitest: specifier: ^3.0.4 - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) example-react: dependencies: @@ -44,7 +44,7 @@ importers: version: 1.50.1 '@tailwindcss/vite': specifier: ^4.0.5 - version: 4.0.5(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) react: specifier: 19.0.0 version: 19.0.0 @@ -59,11 +59,11 @@ importers: version: 4.0.5 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) devDependencies: vite-plugin-inspect: specifier: ^10.1.1 - version: 10.1.1(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 10.1.1(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) openapi-schema-diff: dependencies: @@ -75,8 +75,8 @@ importers: version: 7.6.3 devDependencies: '@types/node': - specifier: ^22.12.0 - version: 22.12.0 + specifier: ^22.13.1 + version: 22.13.1 c8: specifier: ^10.1.3 version: 10.1.3 @@ -157,8 +157,8 @@ importers: specifier: ^4.0.9 version: 4.0.9 '@types/node': - specifier: 22.12.0 - version: 22.12.0 + specifier: 22.13.1 + version: 22.13.1 js-yaml: specifier: ^4.1.0 version: 4.1.0 @@ -170,7 +170,7 @@ importers: version: 0.0.0 '@jacob-ebey/vite-react-server-dom': specifier: ^0.0.12 - version: 0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@modelcontextprotocol/sdk': specifier: ^1.0.4 version: 1.0.4 @@ -179,7 +179,7 @@ importers: version: 0.34.15 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) ajv: specifier: ^8.17.1 version: 8.17.1 @@ -221,7 +221,7 @@ importers: version: 0.0.11(rollup@4.34.6) vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) zod: specifier: ^3.24.1 version: 3.24.1 @@ -233,8 +233,8 @@ importers: specifier: ^4.5.9 version: 4.5.9 '@types/node': - specifier: 22.12.0 - version: 22.12.0 + specifier: 22.13.1 + version: 22.13.1 '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -279,7 +279,7 @@ importers: version: 2.2.1 '@tailwindcss/typography': specifier: ^0.5.13 - version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))) + version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))) '@types/mdx': specifier: ^2.0.13 version: 2.0.13 @@ -313,7 +313,7 @@ importers: version: 4.20250129.0 '@remix-run/dev': specifier: ^2.15.3 - version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) + version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -334,16 +334,16 @@ importers: version: 1.2.0 tailwindcss: specifier: ^3.4.3 - version: 3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + version: 3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) typescript: specifier: ^5.7.3 version: 5.7.3 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + version: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) wrangler: specifier: ^3.48.0 version: 3.107.2(@cloudflare/workers-types@4.20250129.0) @@ -2109,14 +2109,11 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@16.18.123': - resolution: {integrity: sha512-/n7I6V/4agSpJtFDKKFEa763Hc1z3hmvchobHS1TisCOTKD5nxq8NJ2iK7SRIMYL276Q9mgWOx2AWp5n2XI6eA==} + '@types/node@16.18.126': + resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} - '@types/node@22.12.0': - resolution: {integrity: sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==} - - '@types/node@22.13.0': - resolution: {integrity: sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==} + '@types/node@22.13.1': + resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} '@types/react-dom@19.0.3': resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} @@ -7015,13 +7012,13 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@jacob-ebey/vite-react-server-dom@0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@jacob-ebey/vite-react-server-dom@0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@jacob-ebey/react-server-dom-vite': 19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mjackson/node-fetch-server': 0.5.0 - '@vitejs/plugin-react': 4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@vitejs/plugin-react': 4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) unplugin-rsc: 0.0.11(rollup@4.34.6) - vite: 6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - rollup - supports-color @@ -7075,11 +7072,11 @@ snapshots: '@mark.probst/typescript-json-schema@0.55.0': dependencies: '@types/json-schema': 7.0.15 - '@types/node': 16.18.123 + '@types/node': 16.18.126 glob: 7.2.3 path-equal: 1.2.5 safe-stable-stringify: 2.5.0 - ts-node: 10.9.2(@types/node@16.18.123)(typescript@4.9.4) + ts-node: 10.9.2(@types/node@16.18.126)(typescript@4.9.4) typescript: 4.9.4 yargs: 17.7.2 transitivePeerDependencies: @@ -7240,7 +7237,7 @@ snapshots: optionalDependencies: typescript: 5.7.3 - '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': + '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': dependencies: '@babel/core': 7.26.7 '@babel/generator': 7.26.5 @@ -7257,7 +7254,7 @@ snapshots: '@remix-run/router': 1.22.0 '@remix-run/server-runtime': 2.15.3(typescript@5.7.3) '@types/mdx': 2.0.13 - '@vanilla-extract/integration': 6.5.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) + '@vanilla-extract/integration': 6.5.0(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6) arg: 5.0.2 cacache: 17.1.4 chalk: 4.1.2 @@ -7284,7 +7281,7 @@ snapshots: pidtree: 0.6.0 postcss: 8.5.1 postcss-discard-duplicates: 5.1.0(postcss@8.5.1) - postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) postcss-modules: 6.0.1(postcss@8.5.1) prettier: 2.8.8 pretty-ms: 7.0.1 @@ -7296,11 +7293,11 @@ snapshots: tar-fs: 2.1.2 tsconfig-paths: 4.2.0 valibot: 0.41.0(typescript@5.7.3) - vite-node: 1.6.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) + vite-node: 1.6.0(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6) ws: 7.5.10 optionalDependencies: typescript: 5.7.3 - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) wrangler: 3.107.2(@cloudflare/workers-types@4.20250129.0) transitivePeerDependencies: - '@types/node' @@ -7589,21 +7586,21 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.0.5 '@tailwindcss/oxide-win32-x64-msvc': 4.0.5 - '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)))': + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)))': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) - '@tailwindcss/vite@4.0.5(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@tailwindcss/vite@4.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@tailwindcss/node': 4.0.5 '@tailwindcss/oxide': 4.0.5 lightningcss: 1.29.1 tailwindcss: 4.0.5 - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) '@tsconfig/node10@1.0.11': {} @@ -7690,13 +7687,9 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@16.18.123': {} + '@types/node@16.18.126': {} - '@types/node@22.12.0': - dependencies: - undici-types: 6.20.0 - - '@types/node@22.13.0': + '@types/node@22.13.1': dependencies: undici-types: 6.20.0 @@ -7814,21 +7807,21 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - '@vanilla-extract/integration@6.5.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6)': + '@vanilla-extract/integration@6.5.0(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6)': dependencies: '@babel/core': 7.26.7 '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.7) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.1 - esbuild: 0.17.19 + esbuild: 0.17.6 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 lodash: 4.17.21 mlly: 1.7.4 outdent: 0.8.0 - vite: 5.4.14(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) - vite-node: 1.6.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) + vite: 5.4.14(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6) + vite-node: 1.6.0(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7843,14 +7836,14 @@ snapshots: '@vanilla-extract/private@1.0.6': {} - '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.7 '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.7) '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -7861,13 +7854,13 @@ snapshots: chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@vitest/mocker@3.0.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) '@vitest/pretty-format@3.0.4': dependencies: @@ -9011,7 +9004,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.13.0 + '@types/node': 22.13.1 require-like: 0.1.2 event-target-shim@5.0.1: {} @@ -11015,13 +11008,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.1 - postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)): + postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)): dependencies: lilconfig: 3.1.3 yaml: 2.7.0 optionalDependencies: postcss: 8.5.1 - ts-node: 10.9.2(@types/node@22.13.0)(typescript@5.7.3) + ts-node: 10.9.2(@types/node@22.13.1)(typescript@5.7.3) postcss-modules-extract-imports@3.1.0(postcss@8.5.1): dependencies: @@ -11912,7 +11905,7 @@ snapshots: array-back: 6.2.2 wordwrapjs: 5.1.0 - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)): + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -11931,7 +11924,7 @@ snapshots: postcss: 8.5.1 postcss-import: 15.1.0(postcss@8.5.1) postcss-js: 4.0.1(postcss@8.5.1) - postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) postcss-nested: 6.2.0(postcss@8.5.1) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -12036,14 +12029,14 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@16.18.123)(typescript@4.9.4): + ts-node@10.9.2(@types/node@16.18.126)(typescript@4.9.4): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 16.18.123 + '@types/node': 16.18.126 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -12054,14 +12047,14 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3): + ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.13.0 + '@types/node': 22.13.1 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -12383,13 +12376,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@1.6.0(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6): + vite-node@1.6.0(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6): dependencies: cac: 6.7.14 debug: 4.4.0 pathe: 1.1.2 picocolors: 1.1.1 - vite: 5.4.14(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6) + vite: 5.4.14(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6) transitivePeerDependencies: - '@types/node' - less @@ -12401,13 +12394,13 @@ snapshots: - supports-color - terser - vite-node@3.0.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vite-node@3.0.4(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.2 - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -12422,60 +12415,46 @@ snapshots: - tsx - yaml - vite-plugin-inspect@10.1.1(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): + vite-plugin-inspect@10.1.1(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): dependencies: debug: 4.4.0 error-stack-parser-es: 1.0.5 open: 10.1.0 picocolors: 1.1.1 sirv: 3.0.0 - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color - vite-tsconfig-paths@4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): + vite-tsconfig-paths@4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.4(typescript@5.7.3) optionalDependencies: - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) transitivePeerDependencies: - supports-color - typescript - vite@5.4.14(@types/node@22.13.0)(lightningcss@1.29.1)(terser@5.31.6): + vite@5.4.14(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6): dependencies: esbuild: 0.21.5 postcss: 8.5.1 rollup: 4.34.0 optionalDependencies: - '@types/node': 22.13.0 - fsevents: 2.3.3 - lightningcss: 1.29.1 - terser: 5.31.6 - - vite@6.1.0(@types/node@22.12.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): - dependencies: - esbuild: 0.24.2 - postcss: 8.5.1 - rollup: 4.34.6 - optionalDependencies: - '@types/node': 22.12.0 + '@types/node': 22.13.1 fsevents: 2.3.3 - jiti: 2.4.2 lightningcss: 1.29.1 terser: 5.31.6 - tsx: 4.19.2 - yaml: 2.7.0 - vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 postcss: 8.5.1 rollup: 4.34.6 optionalDependencies: - '@types/node': 22.13.0 + '@types/node': 22.13.1 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.1 @@ -12483,10 +12462,10 @@ snapshots: tsx: 4.19.2 yaml: 2.7.0 - vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.4 - '@vitest/mocker': 3.0.4(vite@6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/mocker': 3.0.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.4 '@vitest/runner': 3.0.4 '@vitest/snapshot': 3.0.4 @@ -12502,12 +12481,12 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.1.0(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) - vite-node: 3.0.4(@types/node@22.13.0)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite-node: 3.0.4(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.13.0 + '@types/node': 22.13.1 transitivePeerDependencies: - jiti - less diff --git a/sdk/package.json b/sdk/package.json index 2aed11a..1d4527e 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -12,7 +12,7 @@ "devDependencies": { "@types/diff": "^7.0.0", "@types/js-yaml": "^4.0.9", - "@types/node": "22.12.0", + "@types/node": "22.13.1", "js-yaml": "^4.1.0" }, "dependencies": { diff --git a/spiceflow/package.json b/spiceflow/package.json index 108959f..6a2c030 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -87,7 +87,7 @@ }, "devDependencies": { "@types/lodash.clonedeep": "^4.5.9", - "@types/node": "22.12.0", + "@types/node": "22.13.1", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "eventsource": "^3.0.5", diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index ec6832c..e639d4d 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -15,7 +15,6 @@ export function useFlightData() { export function LayoutContent(props: { id: string }) { const data = useFlightData() - console.log('data', data) const layoutIndex = data.layouts.findIndex((layout) => layout.id === props.id) let nextLayout = data.layouts[layoutIndex + 1]?.element if (nextLayout) { diff --git a/spiceflow/src/react/css.tsx b/spiceflow/src/react/css.tsx new file mode 100644 index 0000000..ec6cdca --- /dev/null +++ b/spiceflow/src/react/css.tsx @@ -0,0 +1,28 @@ +import { DevEnvironment, EnvironmentModuleNode, isCSSRequest } from 'vite' + +export async function collectStyleUrls( + server: DevEnvironment, + { entries }: { entries: string[] }, +) { + const visited = new Set() + + async function traverse(url: string) { + const [, id] = await server.moduleGraph.resolveUrl(url) + const mod = server.moduleGraph.getModuleById(id) + if (!mod || visited.has(mod)) { + return + } + visited.add(mod) + await Promise.all( + [...mod.importedModules].map((childMod) => traverse(childMod.url)), + ) + } + + // ensure import analysis is ready for top entries + await Promise.all(entries.map((e) => server.transformRequest(e))) + + // traverse + await Promise.all(entries.map((url) => traverse(url))) + + return [...visited].map((mod) => mod.url).filter((url) => isCSSRequest(url)) +} diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index f6aa17b..c895aa7 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -102,7 +102,12 @@ const serverReferenceManifest: ServerReferenceManifest = { mod = await import(/* @vite-ignore */ id); } else { const references = await import("virtual:build-server-references"); - mod = await references.default[id](); + const ref = references.default[id] + if (!ref) { + const availableKeys = Object.keys(references.default); + throw new Error(`Could not find server reference for id: ${id}. This likely means the server reference was not properly registered. Available reference keys are: ${availableKeys.join(', ')}`); + } + mod = await ref(); } resolved = mod[name]; }, diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 5e6ae42..b0f4f5e 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -14,9 +14,7 @@ import {injectRSCPayload} from 'rsc-html-stream/server'; import { FlightDataContext } from "./components.js"; import { bootstrapModules } from "virtual:ssr-assets"; import { clientReferenceManifest } from "./utils/client-reference.js"; - - - +import cssUrls from 'virtual:app-styles' export default async function handler( @@ -52,15 +50,16 @@ export default async function handler( ); const ssrAssets = await import("virtual:ssr-assets"); + console.log(cssUrls) - - console.log('payload', payload.root) const el = + {cssUrls.map((url) => ( + + ))} {payload.root?.layouts?.[0]?.element ?? payload.root.page} - console.log('bootstrapModules', bootstrapModules) const htmlStream = fromPipeableToWebReadable( ReactDomServer.renderToPipeableStream(el, { bootstrapModules: ssrAssets.bootstrapModules, diff --git a/spiceflow/src/react/utils/client-reference.ts b/spiceflow/src/react/utils/client-reference.ts index c492bea..467496f 100644 --- a/spiceflow/src/react/utils/client-reference.ts +++ b/spiceflow/src/react/utils/client-reference.ts @@ -19,7 +19,11 @@ export const clientReferenceManifest: ClientReferenceManifest = { const references = await import( 'virtual:build-client-references' as string ) - mod = await references.default[id]() + const ref = references.default[id] + if (!ref) { + throw new Error(`Can't find client reference for module ${id}, among ${Object.keys(references.default).join(', ')}`) + } + mod = await ref() } resolved = mod[name] }, diff --git a/spiceflow/src/spiceflow.ts b/spiceflow/src/spiceflow.ts index bcf4e91..48d32b9 100644 --- a/spiceflow/src/spiceflow.ts +++ b/spiceflow/src/spiceflow.ts @@ -904,7 +904,6 @@ export class Spiceflow< page, layouts, } - console.log(data,) return data } catch (err) { return await getResForError(err) diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index cdc769b..66fbcec 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -19,6 +19,8 @@ import crypto from 'node:crypto' import reactServerDOM from './vite-jacob.js' import { serverTransform, clientTransform } from 'unplugin-rsc' import { noramlizeClientReferenceId } from './react/utils/normalize.js' +import { collectStyleUrls } from './react/css.js' +import { normalizeId } from 'ajv/dist/compile/resolve.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -34,13 +36,14 @@ function makeHash(filename: string) { export function spiceflowPlugin({ entry }): PluginOption { // Move state variables inside plugin closure let browserManifest: Manifest + let rscManifest: Manifest const clientModules = new Map() const serverModules = new Map() let buildScan = false let command: string = '' let server: ViteDevServer - + let buildType: 'scan' | 'server' | 'browser' | 'ssr' | undefined return [ react(), @@ -55,15 +58,9 @@ export function spiceflowPlugin({ entry }): PluginOption { }, async transform(code, id) { - if (id === '\0virtual:react-manifest') { - debugTransformResult({ - envName: this.environment.name, - transformedCode: code, - id, - }) - } let result = code const ext = id.slice(id.lastIndexOf('.')) + if ( EXTENSIONS_TO_TRANSFORM.has(ext) && code.match(/['"]use (client|server)['"]/g) @@ -85,6 +82,7 @@ export function spiceflowPlugin({ entry }): PluginOption { clientModules.set(filename, id) return id } + if (this.environment.name === 'rsc') { const transformed = serverTransform(code, id, { id: generateId, @@ -98,10 +96,7 @@ export function spiceflowPlugin({ entry }): PluginOption { transformedCode: result, id, }) - } else if ( - this.environment.name === 'client' || - this.environment.name === 'ssr' - ) { + } else { const transformed = clientTransform( code, id, @@ -159,6 +154,7 @@ export function spiceflowPlugin({ entry }): PluginOption { rollupOptions: { input: { index: 'spiceflow/dist/react/entry.ssr' }, }, + ssrEmitAssets: true, }, }, rsc: { @@ -172,9 +168,10 @@ export function spiceflowPlugin({ entry }): PluginOption { ], exclude: ['util'], }, + resolve: { conditions: ['react-server'], - // noExternal: ['spiceflow'], + noExternal: ['react', 'react-dom'], }, dev: { createEnvironment(name, config) { @@ -185,7 +182,10 @@ export function spiceflowPlugin({ entry }): PluginOption { }, build: { outDir: 'dist/rsc', + manifest: true, ssr: true, + ssrEmitAssets: true, + emitAssets: true, rollupOptions: { input: { index: 'spiceflow/dist/react/entry.rsc' }, }, @@ -196,6 +196,7 @@ export function spiceflowPlugin({ entry }): PluginOption { sharedPlugins: true, async buildApp(builder) { buildScan = true + // this scan part seems necessary to find all the server references and client references, otherwise they are empty await builder.build(builder.environments.rsc) buildScan = false await builder.build(builder.environments.rsc) @@ -255,6 +256,21 @@ export function spiceflowPlugin({ entry }): PluginOption { createVirtualPlugin('app-entry', () => { return `export {default} from '${url.pathToFileURL(path.resolve(entry))}'` }), + createVirtualPlugin('app-styles', async function () { + if (this.environment.mode !== 'dev') { + const rscCss = Object.values(rscManifest).flatMap((x) => x.css) + const clientCss = Object.values(browserManifest).flatMap((x) => x.css) + + const allStyles = [...rscCss, ...clientCss].filter(Boolean) + return `export default ${JSON.stringify(allStyles)}` + } + const allStyles = await collectStyleUrls(server.environments['rsc'], { + entries: [entry], + }) + const code = `export default ${JSON.stringify(allStyles)}\n\n` + // ensure hmr boundary since css module doesn't have `import.meta.hot.accept` + return code + `if (import.meta.hot) { import.meta.hot.accept() }` + }), createVirtualPlugin('ssr-assets', function () { // TODO this should also add other client modules used to speed loading up during build @@ -313,24 +329,39 @@ export function spiceflowPlugin({ entry }): PluginOption { assert(typeof output.source === 'string') browserManifest = JSON.parse(output.source) } + if (this.environment.name === 'rsc') { + const output = bundle['.vite/manifest.json'] + assert(output.type === 'asset') + assert(typeof output.source === 'string') + rscManifest = JSON.parse(output.source) + } }, }, createVirtualPlugin('build-client-references', () => { - const code = Array.from(clientModules.keys()) - .map( - (id) => `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, - ) - .join('\n') + let result = `export default {\n` + for (let [filename, id] of clientModules) { + // Handle virtual modules by removing \0 prefix if present + const importPath = filename.startsWith('\0') + ? filename.slice(1) + : filename + result += `"${id}": () => import("${importPath}"),\n` + } + result += `};\n` - return `export default {${code}}` + return { code: result, map: null } }), createVirtualPlugin('build-server-references', () => { - const code = Array.from(serverModules.keys()) - .map( - (id) => `${JSON.stringify(id)}: () => import(${JSON.stringify(id)}),`, - ) - .join('\n') - return `export default {${code}}` + let result = `export default {\n` + for (let [filename, id] of serverModules) { + // Handle virtual modules by removing \0 prefix if present + const importPath = filename.startsWith('\0') + ? filename.slice(1) + : filename + result += `"${id}": () => import("${importPath}"),\n` + } + result += `};\n` + + return { code: result, map: null } }), // vitePluginSilenceDirectiveBuildWarning(), @@ -340,6 +371,7 @@ export function spiceflowPlugin({ entry }): PluginOption { name = 'virtual:' + name return { name: `virtual-${name}`, + resolveId(source, _importer, _options) { return source === name ? '\0' + name : undefined }, From 76a569e06e6c9feda2d52d4e6a59c20d6ae43ad9 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 12:36:05 +0100 Subject: [PATCH 038/100] fix missing server references from client components in build --- spiceflow/src/vite.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 66fbcec..4c9eb02 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -65,7 +65,12 @@ export function spiceflowPlugin({ entry }): PluginOption { EXTENSIONS_TO_TRANSFORM.has(ext) && code.match(/['"]use (client|server)['"]/g) ) { - const mod = await server.moduleGraph.getModuleByUrl(id) + const isUseClient = /^\s*(("use client")|('use client'))/.test(code) + if (isUseClient && buildScan) { + // This is needed to let scan discover server references found in the use client components + return + } + const mod = await server?.moduleGraph?.getModuleByUrl(id) let generateId = (filename, directive) => { let id = '' if (command === 'build') { @@ -371,7 +376,7 @@ export function spiceflowPlugin({ entry }): PluginOption { name = 'virtual:' + name return { name: `virtual-${name}`, - + resolveId(source, _importer, _options) { return source === name ? '\0' + name : undefined }, From 1e57519a4b02bd406771a2a744431708db2bb3b5 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 13:05:31 +0100 Subject: [PATCH 039/100] fix css during build, need to copy them, thanks to preserveEntrySignatures that fixes the empty ssr manifest, found this trick from jacob --- spiceflow/src/react/entry.ssr.tsx | 2 - spiceflow/src/vite.tsx | 68 +++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index b0f4f5e..7357f30 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -50,8 +50,6 @@ export default async function handler( ); const ssrAssets = await import("virtual:ssr-assets"); - console.log(cssUrls) - const el = {cssUrls.map((url) => ( diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 4c9eb02..2f88b4c 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -1,4 +1,6 @@ import assert from 'node:assert' +import * as vite from 'vite' + import fs from 'node:fs' import url from 'node:url' import path from 'node:path' @@ -147,6 +149,7 @@ export function spiceflowPlugin({ entry }): PluginOption { }, build: { manifest: true, + outDir: 'dist/client', rollupOptions: { input: { index: 'virtual:browser-entry' }, @@ -155,10 +158,16 @@ export function spiceflowPlugin({ entry }): PluginOption { }, ssr: { build: { + manifest: true, + ssrManifest: true, + outDir: 'dist/ssr', rollupOptions: { + // preserveEntrySignatures: 'exports-only', + input: { index: 'spiceflow/dist/react/entry.ssr' }, }, + emitAssets: true, ssrEmitAssets: true, }, }, @@ -186,12 +195,14 @@ export function spiceflowPlugin({ entry }): PluginOption { }, }, build: { + ssrManifest: true, outDir: 'dist/rsc', manifest: true, - ssr: true, ssrEmitAssets: true, emitAssets: true, rollupOptions: { + preserveEntrySignatures: 'exports-only', + input: { index: 'spiceflow/dist/react/entry.rsc' }, }, }, @@ -204,9 +215,28 @@ export function spiceflowPlugin({ entry }): PluginOption { // this scan part seems necessary to find all the server references and client references, otherwise they are empty await builder.build(builder.environments.rsc) buildScan = false - await builder.build(builder.environments.rsc) - await builder.build(builder.environments.client) - await builder.build(builder.environments.ssr) + const rscOutputs = (await builder.build( + builder.environments.rsc, + )) as vite.Rollup.RollupOutput + const clientOutputs = (await builder.build( + builder.environments.client, + )) as vite.Rollup.RollupOutput + const ssrOutputs = (await builder.build( + builder.environments.ssr, + )) as vite.Rollup.RollupOutput + + const clientOutDir = builder.environments.client.config.build.outDir + + moveStaticAssets( + ssrOutputs, + builder.environments.ssr.config.build.outDir, + clientOutDir, + ) + moveStaticAssets( + rscOutputs, + builder.environments.rsc.config.build.outDir, + clientOutDir, + ) }, }, }), @@ -466,3 +496,33 @@ const EXTENSIONS_TO_TRANSFORM = new Set([ '.mts', '.mtsx', ]) + +function moveStaticAssets( + output: vite.Rollup.RollupOutput, + outDir: string, + clientOutDir: string, +) { + const manifestAsset = output.output.find( + (asset) => asset.fileName === '.vite/ssr-manifest.json', + ) + if (!manifestAsset || manifestAsset.type !== 'asset') { + // console.log(output.output) + throw new Error('could not find manifest') + } + const manifest = JSON.parse(manifestAsset.source as string) + + const processed = new Set() + for (const assets of Object.values(manifest) as string[][]) { + for (const asset of assets) { + const fullPath = path.join(outDir, asset.slice(1)) + + // console.log({ fullPath }) + if (asset.endsWith('.js') || processed.has(fullPath)) continue + processed.add(fullPath) + if (!fs.existsSync(fullPath)) continue + + const relative = path.relative(outDir, fullPath) + fs.renameSync(fullPath, path.join(clientOutDir, relative)) + } + } +} From 41db076a0fbf26fce9186ff2e8670e4365ce8154 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 15:10:29 +0100 Subject: [PATCH 040/100] use routeSorter to make sure static routes have priority --- example-react/e2e/basic.test.ts | 4 +- example-react/src/main.tsx | 3 + spiceflow/src/react.test.ts | 125 +++++++++++++----------------- spiceflow/src/react/entry.rsc.tsx | 2 + spiceflow/src/spiceflow.ts | 41 ++++++---- 5 files changed, 87 insertions(+), 88 deletions(-) diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index a69db13..eb923e2 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -33,6 +33,8 @@ test.describe("redirect", () => { }); + + test.describe(() => { test.use({ javaScriptEnabled: false }); test("server reference in server @nojs", async ({ page }) => { @@ -63,7 +65,7 @@ test("server reference in client @js", async ({ page }) => { test.describe(() => { test.use({ javaScriptEnabled: false }); - test("server reference in client @nojs", async ({ page }) => { + test.skip("server reference in client @nojs", async ({ page }) => { await testServerAction2(page, { js: false }); }); }); diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index f94b75c..8d76c39 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -65,6 +65,9 @@ const app = new Spiceflow() ); }) + .page('/rsc-error', async () => { + throw new Error('test error'); + }) .page("/redirect-in-rsc", async () => { return ; }) diff --git a/spiceflow/src/react.test.ts b/spiceflow/src/react.test.ts index aa00bff..f9ae1eb 100644 --- a/spiceflow/src/react.test.ts +++ b/spiceflow/src/react.test.ts @@ -4,16 +4,52 @@ import { bfs, cloneDeep, Spiceflow } from './spiceflow.js' import { z } from 'zod' import { createSpiceflowClient } from './client/index.js' -test.skip('layout and page work together', async () => { +test('layout and page work together', async () => { const res = await new Spiceflow() .layout('/xxx', () => ({ layout: 'layout' })) - .post('/xxx', () => ({ page: 'page' })) + .page('/xxx', () => ({ page: 'page' })) .handle(new Request('http://localhost/xxx', { method: 'POST' })) - expect(res.status).toBe(200) - expect(await res.json()).toEqual({ - layout: 'layout', - page: 'page', - }) + + expect(res).toMatchInlineSnapshot(` + { + "layouts": [ + { + "element": { + "layout": "layout", + }, + "id": "layout-post--xxx", + }, + ], + "page": { + "page": "page", + }, + "url": "http://localhost/xxx", + } + `) +}) +test('layout and page, static routes have priority', async () => { + const res = await new Spiceflow() + .layout('/xxx', () => ({ layout: 'layout' })) + .page('/:id', () => ({ page: ':id' })) + .page('/xxx', () => ({ page: 'page' })) + .handle(new Request('http://localhost/xxx', { method: 'POST' })) + + expect(res).toMatchInlineSnapshot(` + { + "layouts": [ + { + "element": { + "layout": "layout", + }, + "id": "layout-post--xxx", + }, + ], + "page": { + "page": "page", + }, + "url": "http://localhost/xxx", + } + `) }) test('layout and page work together with params', async () => { @@ -27,6 +63,7 @@ test('layout and page work together with params', async () => { { "handler": [Function], "hooks": undefined, + "id": "layout-get--", "kind": "layout", "method": "GET", "path": "/", @@ -38,6 +75,7 @@ test('layout and page work together with params', async () => { { "handler": [Function], "hooks": undefined, + "id": "layout-post--", "kind": "layout", "method": "POST", "path": "/", @@ -49,6 +87,7 @@ test('layout and page work together with params', async () => { { "handler": [Function], "hooks": undefined, + "id": "page-get--:id", "kind": "page", "method": "GET", "path": "/:id", @@ -60,6 +99,7 @@ test('layout and page work together with params', async () => { { "handler": [Function], "hooks": undefined, + "id": "page-post--:id", "kind": "page", "method": "POST", "path": "/:id", @@ -80,73 +120,12 @@ test('layout and page work together with params', async () => { `) expect(await res).toMatchInlineSnapshot(` - Response { - Symbol(state): { - "aborted": false, - "body": { - "length": 65, - "source": "{"message":"Cannot read properties of undefined (reading 'map')"}", - "stream": ReadableStream { - Symbol(kType): "ReadableStream", - Symbol(kState): { - "controller": ReadableByteStreamController { - Symbol(kType): "ReadableByteStreamController", - Symbol(kState): { - "autoAllocateChunkSize": undefined, - "byobRequest": null, - "cancelAlgorithm": [Function], - "closeRequested": false, - "highWaterMark": 0, - "pendingPullIntos": [], - "pullAgain": false, - "pullAlgorithm": [Function], - "pulling": false, - "queue": [], - "queueTotalSize": 0, - "started": true, - "stream": [Circular], - }, - }, - "disturbed": false, - "reader": undefined, - "state": "readable", - "storedError": undefined, - "transfer": { - "port1": undefined, - "port2": undefined, - "promise": undefined, - "writable": undefined, - }, - }, - Symbol(nodejs.webstream.isClosedPromise): { - "promise": Promise {}, - "reject": [Function], - "resolve": [Function], - }, - Symbol(nodejs.webstream.controllerErrorFunction): [Function], - }, - }, - "cacheState": "", - "headersList": HeadersList { - "cookies": null, - Symbol(headers map): Map { - "content-type" => { - "name": "content-type", - "value": "application/json", - }, - }, - Symbol(headers map sorted): null, - }, - "rangeRequested": false, - "requestIncludesCredentials": false, - "status": 500, - "statusText": "", - "timingAllowPassed": false, - "timingInfo": null, - "type": "default", - "urlList": [], + { + "layouts": [], + "page": { + "page": "123", }, - Symbol(headers): Headers {}, + "url": "http://localhost/123", } `) }) diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index c895aa7..5cdc828 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -46,6 +46,7 @@ export async function handler( } else { // progressive enhancement const formData = await request.formData(); + console.log(formData); const decodedAction = await ReactServer.decodeAction( formData, serverReferenceManifest, @@ -93,6 +94,7 @@ export async function handler( const serverReferenceManifest: ServerReferenceManifest = { resolveServerReference(reference: string) { + const [id, name] = reference.split("#"); let resolved: unknown; return { diff --git a/spiceflow/src/spiceflow.ts b/spiceflow/src/spiceflow.ts index 48d32b9..af2bfe3 100644 --- a/spiceflow/src/spiceflow.ts +++ b/spiceflow/src/spiceflow.ts @@ -188,23 +188,25 @@ export class Spiceflow< } private getAllDecodedParams( - matchResult: Result, + _matchResult: Result, pathname: string, routeIndex, ): Record { - if (!matchResult?.length || !matchResult?.[0]?.[routeIndex]?.[1]) { + if (!_matchResult?.length || !_matchResult?.[0]?.[routeIndex]?.[1]) { return {} } + + const matches = _matchResult[0] const internalRoute = - matchResult[0].find(([route]) => route.path.includes('*'))?.[0] || - matchResult[0][routeIndex][0] + matches.find(([route]) => route.path.includes('*'))?.[0] || + matches[routeIndex][0] const decoded: Record = extractWildcardParam(pathname, internalRoute?.path) || {} - const keys = Object.keys(matchResult[0][routeIndex][1]) + const keys = Object.keys(matches[routeIndex][1]) for (const key of keys) { - const value = matchResult[0][routeIndex][1][key] + const value = matches[routeIndex][1][key] if (value) { decoded[key] = /\%/.test(value) ? decodeURIComponent_(value) : value } @@ -240,11 +242,15 @@ export class Spiceflow< } // Get all matched routes - const routes = matchedRoutes[0].map(([route, params], index) => ({ - app, - route, - params: this.getAllDecodedParams(matchedRoutes, originalPath, index), - })) + const routes = matchedRoutes[0] + .map(([route, params], index) => ({ + app, + route, + params: this.getAllDecodedParams(matchedRoutes, originalPath, index), + })) + .sort((a, b) => { + return routeSorter(a.route, b.route) + }) if (routes.length) { return routes @@ -1591,10 +1597,17 @@ export function cloneDeep(x) { return lodashCloneDeep(x) } -console.log(`piceflow running`) +function routeSorter(a: InternalRoute, b: InternalRoute) { + // Count dynamic parameters (:param and *) in each route + const aCount = a.path + .split('/') + .filter((p) => p.startsWith(':') || p === '*').length + const bCount = b.path + .split('/') + .filter((p) => p.startsWith(':') || p === '*').length + return aCount - bCount +} -const tryDecodeURIComponent = (str: string) => - tryDecode(str, decodeURIComponent_) function partition(arr: T[], predicate: (item: T) => boolean): [T[], T[]] { return arr.reduce( (acc, item) => { From bffac7d592b2fb82e9bd5b307f442e3cad7f6c9e Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 16:33:47 +0100 Subject: [PATCH 041/100] checking what happens on errors --- example-react/src/app/client.tsx | 5 +++++ example-react/src/app/index.tsx | 2 ++ example-react/src/main.tsx | 15 ++++++++++++++- example-react/tsconfig.json | 1 - 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index 1719baa..12d2afa 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -65,3 +65,8 @@ export function Calculator() { + +export function ClientComponentThrows() { + throw new Error('Client component error'); + return
Client component
; +} diff --git a/example-react/src/app/index.tsx b/example-react/src/app/index.tsx index fa69251..6f8452c 100644 --- a/example-react/src/app/index.tsx +++ b/example-react/src/app/index.tsx @@ -27,3 +27,5 @@ export async function IndexPage() { ); } + + diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 8d76c39..172aea9 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -2,6 +2,7 @@ import { Spiceflow } from "spiceflow"; import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; import './styles.css' +import { ClientComponentThrows } from "./app/client"; const app = new Spiceflow() @@ -65,8 +66,14 @@ const app = new Spiceflow() ); }) - .page('/rsc-error', async () => { + .page('/loader-error', async () => { throw new Error('test error'); + }) + .page('/rsc-error', async () => { + return + }) + .page('/client-error', async () => { + return }) .page("/redirect-in-rsc", async () => { return ; @@ -86,4 +93,10 @@ async function Redirects() { return
Redirect
; } +function ServerComponentThrows() { + throw new Error('Server component error'); + return
Server component
; +} + + export default app; diff --git a/example-react/tsconfig.json b/example-react/tsconfig.json index 2bcdce2..5b56fc6 100644 --- a/example-react/tsconfig.json +++ b/example-react/tsconfig.json @@ -2,7 +2,6 @@ "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2022", "esnext"], "types": ["vite/client"], - "rootDirs": ["src", "e2e"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", From e75204fd20945c3f0399c360c974a7c44a3f9df2 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 16:34:30 +0100 Subject: [PATCH 042/100] run prettier --- spiceflow/src/client/types.ts | 92 ++++--- spiceflow/src/cors.test.ts | 16 +- spiceflow/src/mcp.ts | 2 +- spiceflow/src/middleware.test.ts | 30 ++- spiceflow/src/react/components.tsx | 2 +- spiceflow/src/react/entry.client.tsx | 239 +++++++++--------- spiceflow/src/react/entry.rsc.tsx | 209 ++++++++------- spiceflow/src/react/entry.ssr.tsx | 147 ++++++----- spiceflow/src/react/references.browser.tsx | 6 +- .../src/react/server-dom-client-optimized.tsx | 6 +- spiceflow/src/react/server-dom-optimized.tsx | 8 +- spiceflow/src/react/types/ambient.d.ts | 115 +++++---- spiceflow/src/react/types/index.ts | 26 +- spiceflow/src/react/utils/client-reference.ts | 12 +- spiceflow/src/react/utils/fetch.ts | 122 ++++----- spiceflow/src/react/utils/normalize.ts | 4 +- spiceflow/src/spiceflow.test.ts | 5 +- spiceflow/src/trie-router/node.ts | 55 +++- spiceflow/src/trie-router/router.ts | 2 - spiceflow/src/trie-router/url.ts | 54 +++- spiceflow/src/trie-router/utils.ts | 3 +- spiceflow/src/types.ts | 239 +++++++++--------- spiceflow/src/vite-jacob.ts | 4 +- 23 files changed, 717 insertions(+), 681 deletions(-) diff --git a/spiceflow/src/client/types.ts b/spiceflow/src/client/types.ts index ca27add..70a697b 100644 --- a/spiceflow/src/client/types.ts +++ b/spiceflow/src/client/types.ts @@ -15,11 +15,11 @@ type ReplaceBlobWithFiles> = { [K in keyof RecordType]: RecordType[K] extends any ? RecordType[K] : RecordType[K] extends - | Blob - | Blob[] - | { arrayBuffer: () => Promise } - ? Files - : RecordType[K] + | Blob + | Blob[] + | { arrayBuffer: () => Promise } + ? Files + : RecordType[K] } & {} type And = A extends true @@ -34,18 +34,18 @@ type ReplaceGeneratorWithAsyncGenerator< [K in keyof RecordType]: RecordType[K] extends any ? RecordType[K] : RecordType[K] extends Generator - ? And>, void extends B ? true : false> extends true - ? AsyncGenerator - : And, void extends B ? false : true> extends true - ? B - : AsyncGenerator | B - : RecordType[K] extends AsyncGenerator - ? And>, void extends B ? true : false> extends true - ? AsyncGenerator - : And, void extends B ? false : true> extends true - ? B - : AsyncGenerator | B - : RecordType[K] + ? And>, void extends B ? true : false> extends true + ? AsyncGenerator + : And, void extends B ? false : true> extends true + ? B + : AsyncGenerator | B + : RecordType[K] extends AsyncGenerator + ? And>, void extends B ? true : false> extends true + ? AsyncGenerator + : And, void extends B ? false : true> extends true + ? B + : AsyncGenerator | B + : RecordType[K] } & {} type MaybeArray = T | T[] @@ -97,40 +97,38 @@ export namespace SpiceflowClient { ClientResponse> > : K extends 'get' | 'head' - ? ( - options: Prettify, - ) => Promise< - ClientResponse> - > - : ( - body: Body extends Record - ? ReplaceBlobWithFiles - : Body, - options: Prettify, - ) => Promise< - ClientResponse> - > + ? ( + options: Prettify, + ) => Promise< + ClientResponse> + > + : ( + body: Body extends Record + ? ReplaceBlobWithFiles + : Body, + options: Prettify, + ) => Promise< + ClientResponse> + > : never : CreateParams } - type CreateParams> = Extract< - keyof Route, - `:${string}` - > extends infer Path extends string - ? IsNever extends true - ? Prettify> - : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED - (((params: { - [param in Path extends `:${infer Param}` - ? Param extends `${infer Param}?` - ? Param - : Param - : never]: string | number - }) => Prettify> & CreateParams) & - Prettify>) & - (Path extends `:${string}?` ? CreateParams : {}) - : never + type CreateParams> = + Extract extends infer Path extends string + ? IsNever extends true + ? Prettify> + : // ! DO NOT USE PRETTIFY ON THIS LINE, OTHERWISE FUNCTION CALLING WILL BE OMITTED + (((params: { + [param in Path extends `:${infer Param}` + ? Param extends `${infer Param}?` + ? Param + : Param + : never]: string | number + }) => Prettify> & CreateParams) & + Prettify>) & + (Path extends `:${string}?` ? CreateParams : {}) + : never export interface Config { // fetch?: Omit diff --git a/spiceflow/src/cors.test.ts b/spiceflow/src/cors.test.ts index a09a7e7..faac4f3 100644 --- a/spiceflow/src/cors.test.ts +++ b/spiceflow/src/cors.test.ts @@ -50,9 +50,9 @@ describe('cors middleware', () => { }) test('CORS headers are set when an error is thrown', async () => { - let errorRouteCallCount = 0; + let errorRouteCallCount = 0 const errorApp = new Spiceflow().use(cors()).get('/error', () => { - errorRouteCallCount++; + errorRouteCallCount++ throw new Error('Test error') }) @@ -64,29 +64,31 @@ test('CORS headers are set when an error is thrown', async () => { }) test('CORS headers are set for OPTIONS request when an error is thrown', async () => { - let errorRouteCallCount = 0; + let errorRouteCallCount = 0 const errorApp = new Spiceflow().use(cors()).options('/error', () => { - errorRouteCallCount++; + errorRouteCallCount++ throw new Error('Test error') }) const res = await errorApp.handle(request('error', 'OPTIONS')) expect(res.status).toBe(204) expect(res.headers.get('Access-Control-Allow-Origin')).toBe('*') - expect(res.headers.get('Access-Control-Allow-Methods')).toBe('GET,HEAD,PUT,POST,DELETE,PATCH') + expect(res.headers.get('Access-Control-Allow-Methods')).toBe( + 'GET,HEAD,PUT,POST,DELETE,PATCH', + ) expect(errorRouteCallCount).toBe(1) }) // TODO should middleware errors be handled? errors can be a way to short circuit other middlewares test('CORS headers are set when an error is thrown in middleware', async () => { - let errorRouteCallCount = 0; + let errorRouteCallCount = 0 const errorApp = new Spiceflow() .use((c) => { throw new Error('middleware error') }) .use(cors()) .get('/error', () => { - errorRouteCallCount++; + errorRouteCallCount++ throw new Error('Test error') }) diff --git a/spiceflow/src/mcp.ts b/spiceflow/src/mcp.ts index 3f1b443..62faa51 100644 --- a/spiceflow/src/mcp.ts +++ b/spiceflow/src/mcp.ts @@ -45,7 +45,7 @@ function getOperationParameters(operation: OpenAPIV3.OperationObject): { operation.parameters.forEach((param) => { if ('$ref' in param) return // TODO referenced parameters - + if (param.in === 'query') { queryProperties[param.name] = param.schema as OpenAPIV3.SchemaObject if (param.required) queryRequired.push(param.name) diff --git a/spiceflow/src/middleware.test.ts b/spiceflow/src/middleware.test.ts index 833b648..644d075 100644 --- a/spiceflow/src/middleware.test.ts +++ b/spiceflow/src/middleware.test.ts @@ -37,7 +37,6 @@ test('middleware with no handlers works', async () => { expect(await res.text()).toEqual('ok') }) - test('middleware calling next() without returning it works', async () => { const res = await new Spiceflow() .use(async ({ request }, next) => { @@ -76,7 +75,6 @@ test('middleware state is not shared between requests', async () => { .state('x', -1) .use(async ({ request, state, query }, next) => { state.x = Number(query?.x || -1) - }) .get('/get', ({ state }) => { return state.x @@ -333,7 +331,6 @@ test('middleware returning response and middleware adding header with mounted Sp expect(res.headers.get('X-Added-Header')).toBe('HeaderValue') }) - test('each middleware and route is called exactly once if an error is thrown', async () => { const callOrder: string[] = [] @@ -359,20 +356,29 @@ test('each middleware and route is called exactly once if an error is thrown', a const res = await app.handle(new Request('http://localhost/test')) expect(res.status).toBe(500) - expect(await res.text()).toMatchInlineSnapshot(`"{"message":"Route response"}"`) - expect(callOrder).toEqual(['middleware1', 'middleware2', 'middleware3', 'route']) - + expect(await res.text()).toMatchInlineSnapshot( + `"{"message":"Route response"}"`, + ) + expect(callOrder).toEqual([ + 'middleware1', + 'middleware2', + 'middleware3', + 'route', + ]) + // Check that each middleware and route is called exactly once - const counts = callOrder.reduce((acc, item) => { - acc[item] = (acc[item] || 0) + 1 - return acc - }, {} as Record) + const counts = callOrder.reduce( + (acc, item) => { + acc[item] = (acc[item] || 0) + 1 + return acc + }, + {} as Record, + ) expect(counts).toEqual({ middleware1: 1, middleware2: 1, middleware3: 1, - route: 1 + route: 1, }) }) - diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index e639d4d..8bd2caf 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -20,7 +20,7 @@ export function LayoutContent(props: { id: string }) { if (nextLayout) { return nextLayout } - + return data.page } diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 490cbd2..2979e7e 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,126 +1,127 @@ -import React from "react"; -import ReactDomClient from "react-dom/client"; -import ReactClient from "spiceflow/dist/react/server-dom-client-optimized"; -import type { ServerPayload } from "./entry.rsc.js"; -import type { CallServerFn } from "./types/index.js"; -import { clientReferenceManifest } from "./utils/client-reference.js"; -import {rscStream} from 'rsc-html-stream/client'; -import { FlightDataContext } from "./components.js"; - +import React from 'react' +import ReactDomClient from 'react-dom/client' +import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' +import type { ServerPayload } from './entry.rsc.js' +import type { CallServerFn } from './types/index.js' +import { clientReferenceManifest } from './utils/client-reference.js' +import { rscStream } from 'rsc-html-stream/client' +import { FlightDataContext } from './components.js' async function main() { - const callServer: CallServerFn = async (id, args) => { - const url = new URL(window.location.href); - url.searchParams.set("__rsc", id); - const payload = await ReactClient.createFromFetch( - fetch(url, { - method: "POST", - body: await ReactClient.encodeReply(args), - }), - clientReferenceManifest, - { callServer }, - ); - setPayload(payload); - return payload.returnValue; - }; - Object.assign(globalThis, { __callServer: callServer }); - - async function onNavigation() { - const url = new URL(window.location.href); - url.searchParams.set("__rsc", ""); - const payload = await ReactClient.createFromFetch( - fetch(url), - clientReferenceManifest, - - { callServer }, - ); - setPayload(payload); - } - - const initialPayload = - await ReactClient.createFromReadableStream( - rscStream, - clientReferenceManifest, - - { callServer }, - ); - - let setPayload: (v: ServerPayload) => void; - - function BrowserRoot() { - const [payload, setPayload_] = React.useState(initialPayload); - const [_isPending, startTransition] = React.useTransition(); - - React.useEffect(() => { - setPayload = (v) => startTransition(() => setPayload_(v)); - }, [startTransition, setPayload_]); - - React.useEffect(() => { - return listenNavigation(onNavigation); - }, []); - - return - {payload.root?.layouts?.[0]?.element ?? payload.root.page} - - } - - ReactDomClient.hydrateRoot(document, , { - formState: initialPayload.formState, - }); - - if (import.meta.hot) { - import.meta.hot.on("react-server:update", (e) => { - console.log("[react-server:update]", e.file); - window.history.replaceState({}, "", window.location.href); - }); - } + const callServer: CallServerFn = async (id, args) => { + const url = new URL(window.location.href) + url.searchParams.set('__rsc', id) + const payload = await ReactClient.createFromFetch( + fetch(url, { + method: 'POST', + body: await ReactClient.encodeReply(args), + }), + clientReferenceManifest, + { callServer }, + ) + setPayload(payload) + return payload.returnValue + } + Object.assign(globalThis, { __callServer: callServer }) + + async function onNavigation() { + const url = new URL(window.location.href) + url.searchParams.set('__rsc', '') + const payload = await ReactClient.createFromFetch( + fetch(url), + clientReferenceManifest, + + { callServer }, + ) + setPayload(payload) + } + + const initialPayload = + await ReactClient.createFromReadableStream( + rscStream, + clientReferenceManifest, + + { callServer }, + ) + + let setPayload: (v: ServerPayload) => void + + function BrowserRoot() { + const [payload, setPayload_] = React.useState(initialPayload) + const [_isPending, startTransition] = React.useTransition() + + React.useEffect(() => { + setPayload = (v) => startTransition(() => setPayload_(v)) + }, [startTransition, setPayload_]) + + React.useEffect(() => { + return listenNavigation(onNavigation) + }, []) + + return ( + + {payload.root?.layouts?.[0]?.element ?? payload.root.page} + + ) + } + + ReactDomClient.hydrateRoot(document, , { + formState: initialPayload.formState, + }) + + if (import.meta.hot) { + import.meta.hot.on('react-server:update', (e) => { + console.log('[react-server:update]', e.file) + window.history.replaceState({}, '', window.location.href) + }) + } } function listenNavigation(onNavigation: () => void) { - window.addEventListener("popstate", onNavigation); - - const oldPushState = window.history.pushState; - window.history.pushState = function (...args) { - const res = oldPushState.apply(this, args); - onNavigation(); - return res; - }; - - const oldReplaceState = window.history.replaceState; - window.history.replaceState = function (...args) { - const res = oldReplaceState.apply(this, args); - onNavigation(); - return res; - }; - - function onClick(e: MouseEvent) { - let link = (e.target as Element).closest("a"); - if ( - link && - link instanceof HTMLAnchorElement && - link.href && - (!link.target || link.target === "_self") && - link.origin === location.origin && - !link.hasAttribute("download") && - e.button === 0 && // left clicks only - !e.metaKey && // open in new tab (mac) - !e.ctrlKey && // open in new tab (windows) - !e.altKey && // download - !e.shiftKey && - !e.defaultPrevented - ) { - e.preventDefault(); - history.pushState(null, "", link.href); - } - } - document.addEventListener("click", onClick); - - return () => { - document.removeEventListener("click", onClick); - window.removeEventListener("popstate", onNavigation); - window.history.pushState = oldPushState; - window.history.replaceState = oldReplaceState; - }; + window.addEventListener('popstate', onNavigation) + + const oldPushState = window.history.pushState + window.history.pushState = function (...args) { + const res = oldPushState.apply(this, args) + onNavigation() + return res + } + + const oldReplaceState = window.history.replaceState + window.history.replaceState = function (...args) { + const res = oldReplaceState.apply(this, args) + onNavigation() + return res + } + + function onClick(e: MouseEvent) { + let link = (e.target as Element).closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + e.button === 0 && // left clicks only + !e.metaKey && // open in new tab (mac) + !e.ctrlKey && // open in new tab (windows) + !e.altKey && // download + !e.shiftKey && + !e.defaultPrevented + ) { + e.preventDefault() + history.pushState(null, '', link.href) + } + } + document.addEventListener('click', onClick) + + return () => { + document.removeEventListener('click', onClick) + window.removeEventListener('popstate', onNavigation) + window.history.pushState = oldPushState + window.history.replaceState = oldReplaceState + } } -main(); +main() diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index 5cdc828..10437c3 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -1,129 +1,118 @@ -import type { ReactFormState } from "react-dom/client"; -import React from "react"; -import ReactServer from "spiceflow/dist/react/server-dom-optimized"; +import type { ReactFormState } from 'react-dom/client' +import React from 'react' +import ReactServer from 'spiceflow/dist/react/server-dom-optimized' import type { - ClientReferenceMetadataManifest, - ServerReferenceManifest, -} from "./types/index.js"; + ClientReferenceMetadataManifest, + ServerReferenceManifest, +} from './types/index.js' import app from 'virtual:app-entry' -import { fromPipeableToWebReadable } from "./utils/fetch.js"; -import { FlightData } from "./components.js"; - +import { fromPipeableToWebReadable } from './utils/fetch.js' +import { FlightData } from './components.js' export interface RscHandlerResult { - stream: ReadableStream; + stream: ReadableStream } export interface ServerPayload { - root: FlightData; - formState?: ReactFormState; - returnValue?: unknown; + root: FlightData + formState?: ReactFormState + returnValue?: unknown } +export async function handler(url: URL, request: Request) { + // handle action + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + if (request.method === 'POST') { + const actionId = url.searchParams.get('__rsc') + if (actionId) { + // client stream request + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + const args = await ReactServer.decodeReply(body) + const reference = serverReferenceManifest.resolveServerReference(actionId) + await reference.preload() + const action = await reference.get() + returnValue = await (action as any).apply(null, args) + } else { + // progressive enhancement + const formData = await request.formData() + console.log(formData) + const decodedAction = await ReactServer.decodeAction( + formData, + serverReferenceManifest, + ) + formState = await ReactServer.decodeFormState( + await decodedAction(), + formData, + serverReferenceManifest, + ) + } + } -export async function handler( - url: URL, - request: Request, -) { - // handle action - let returnValue: unknown | undefined; - let formState: ReactFormState | undefined; - if (request.method === "POST") { - const actionId = url.searchParams.get("__rsc"); - if (actionId) { - // client stream request - const contentType = request.headers.get("content-type"); - const body = contentType?.startsWith("multipart/form-data") - ? await request.formData() - : await request.text(); - const args = await ReactServer.decodeReply(body); - const reference = - serverReferenceManifest.resolveServerReference(actionId); - await reference.preload(); - const action = await reference.get(); - returnValue = await (action as any).apply(null, args); - } else { - // progressive enhancement - const formData = await request.formData(); - console.log(formData); - const decodedAction = await ReactServer.decodeAction( - formData, - serverReferenceManifest, - ); - formState = await ReactServer.decodeFormState( - await decodedAction(), - formData, - serverReferenceManifest, - ); - } - } + const root = await app.handle(request) - const root = await app.handle(request) - - if (root instanceof Response) { - return root - } - const {page, layouts} = root + if (root instanceof Response) { + return root + } + const { page, layouts } = root - let abortable = ReactServer.renderToPipeableStream( - { - root, - returnValue, - formState, - }, - clientReferenceMetadataManifest, - {onError(error) { - - },}, - ) - // render flight stream - const stream = fromPipeableToWebReadable( - abortable - ); - request.signal.addEventListener('abort', () => { - abortable.abort() - }) + let abortable = ReactServer.renderToPipeableStream( + { + root, + returnValue, + formState, + }, + clientReferenceMetadataManifest, + { onError(error) {} }, + ) + // render flight stream + const stream = fromPipeableToWebReadable(abortable) + request.signal.addEventListener('abort', () => { + abortable.abort() + }) - let r : RscHandlerResult = { - stream, - }; - return r + let r: RscHandlerResult = { + stream, + } + return r } - const serverReferenceManifest: ServerReferenceManifest = { - resolveServerReference(reference: string) { - - const [id, name] = reference.split("#"); - let resolved: unknown; - return { - async preload() { - let mod: Record; - if (import.meta.env.DEV) { - mod = await import(/* @vite-ignore */ id); - } else { - const references = await import("virtual:build-server-references"); - const ref = references.default[id] - if (!ref) { - const availableKeys = Object.keys(references.default); - throw new Error(`Could not find server reference for id: ${id}. This likely means the server reference was not properly registered. Available reference keys are: ${availableKeys.join(', ')}`); - } - mod = await ref(); - } - resolved = mod[name]; - }, - get() { - return resolved; - }, - }; - }, -}; + resolveServerReference(reference: string) { + const [id, name] = reference.split('#') + let resolved: unknown + return { + async preload() { + let mod: Record + if (import.meta.env.DEV) { + mod = await import(/* @vite-ignore */ id) + } else { + const references = await import('virtual:build-server-references') + const ref = references.default[id] + if (!ref) { + const availableKeys = Object.keys(references.default) + throw new Error( + `Could not find server reference for id: ${id}. This likely means the server reference was not properly registered. Available reference keys are: ${availableKeys.join(', ')}`, + ) + } + mod = await ref() + } + resolved = mod[name] + }, + get() { + return resolved + }, + } + }, +} const clientReferenceMetadataManifest: ClientReferenceMetadataManifest = { - resolveClientReferenceMetadata(metadata) { - // console.log("[debug:resolveClientReferenceMetadata]", { metadata }, Object.getOwnPropertyDescriptors(metadata)); - return metadata.$$id; - }, -}; - + resolveClientReferenceMetadata(metadata) { + // console.log("[debug:resolveClientReferenceMetadata]", { metadata }, Object.getOwnPropertyDescriptors(metadata)); + return metadata.$$id + }, +} diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 7357f30..1dc4d1a 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -1,91 +1,88 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; -import ReactDomServer from "react-dom/server"; -import ReactClient from "spiceflow/dist/react/server-dom-client-optimized"; -import type { ModuleRunner } from "vite/module-runner"; -import type { ServerPayload } from "./entry.rsc.js"; +import type { IncomingMessage, ServerResponse } from 'node:http' +import ReactDomServer from 'react-dom/server' +import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' +import type { ModuleRunner } from 'vite/module-runner' +import type { ServerPayload } from './entry.rsc.js' import { - createRequest, - fromPipeableToWebReadable, - fromWebToNodeReadable, - sendResponse, -} from "./utils/fetch.js"; -import {injectRSCPayload} from 'rsc-html-stream/server'; -import { FlightDataContext } from "./components.js"; -import { bootstrapModules } from "virtual:ssr-assets"; -import { clientReferenceManifest } from "./utils/client-reference.js"; + createRequest, + fromPipeableToWebReadable, + fromWebToNodeReadable, + sendResponse, +} from './utils/fetch.js' +import { injectRSCPayload } from 'rsc-html-stream/server' +import { FlightDataContext } from './components.js' +import { bootstrapModules } from 'virtual:ssr-assets' +import { clientReferenceManifest } from './utils/client-reference.js' import cssUrls from 'virtual:app-styles' - export default async function handler( - req: IncomingMessage, - res: ServerResponse, + req: IncomingMessage, + res: ServerResponse, ) { - const request = createRequest(req, res); - const url = new URL(request.url); - const rscEntry = await importRscEntry(); - const rscResult = await rscEntry.handler(url, request); + const request = createRequest(req, res) + const url = new URL(request.url) + const rscEntry = await importRscEntry() + const rscResult = await rscEntry.handler(url, request) + + if (rscResult instanceof Response) { + sendResponse(rscResult, res) + return + } + + if (url.searchParams.has('__rsc')) { + const response = new Response(rscResult.stream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + }, + }) + sendResponse(response, res) + return + } - if (rscResult instanceof Response) { - sendResponse(rscResult, res); - return; - } - + const [flightStream1, flightStream2] = rscResult.stream.tee() - if (url.searchParams.has("__rsc")) { - const response = new Response(rscResult.stream, { - headers: { - "content-type": "text/x-component;charset=utf-8", - }, - }); - sendResponse(response, res); - return; - } + const payload = await ReactClient.createFromNodeStream( + fromWebToNodeReadable(flightStream1), + clientReferenceManifest, + ) + const ssrAssets = await import('virtual:ssr-assets') - const [flightStream1, flightStream2] = rscResult.stream.tee(); + const el = ( + + {cssUrls.map((url) => ( + + ))} + {payload.root?.layouts?.[0]?.element ?? payload.root.page} + + ) - const payload = await ReactClient.createFromNodeStream( - fromWebToNodeReadable(flightStream1), - clientReferenceManifest, - ); - const ssrAssets = await import("virtual:ssr-assets"); + const htmlStream = fromPipeableToWebReadable( + ReactDomServer.renderToPipeableStream(el, { + bootstrapModules: ssrAssets.bootstrapModules, - - const el = - {cssUrls.map((url) => ( - - ))} - {payload.root?.layouts?.[0]?.element ?? payload.root.page} - - - const htmlStream = fromPipeableToWebReadable( - ReactDomServer.renderToPipeableStream(el, { - bootstrapModules: ssrAssets.bootstrapModules, - - // @ts-ignore no type? - formState: payload.formState, - }), - ); + // @ts-ignore no type? + formState: payload.formState, + }), + ) - const response = new Response( - htmlStream - - .pipeThrough(injectRSCPayload(flightStream2)), - { - headers: { - "content-type": "text/html;charset=utf-8", - }, - }, - ); - sendResponse(response, res); + const response = new Response( + htmlStream.pipeThrough(injectRSCPayload(flightStream2)), + { + headers: { + 'content-type': 'text/html;charset=utf-8', + }, + }, + ) + sendResponse(response, res) } -declare let __rscRunner: ModuleRunner; +declare let __rscRunner: ModuleRunner -async function importRscEntry(): Promise { - if (import.meta.env.DEV) { - return await __rscRunner.import("spiceflow/dist/react/entry.rsc"); - } else { - return await import("virtual:build-rsc-entry" as any); - } +async function importRscEntry(): Promise { + if (import.meta.env.DEV) { + return await __rscRunner.import('spiceflow/dist/react/entry.rsc') + } else { + return await import('virtual:build-rsc-entry' as any) + } } diff --git a/spiceflow/src/react/references.browser.tsx b/spiceflow/src/react/references.browser.tsx index 93a9d6a..446d9b0 100644 --- a/spiceflow/src/react/references.browser.tsx +++ b/spiceflow/src/react/references.browser.tsx @@ -1,8 +1,4 @@ -import { - createServerReference as createServerReferenceImp -} from 'react-server-dom-vite/client' - - +import { createServerReference as createServerReferenceImp } from 'react-server-dom-vite/client' export function createServerReference(imp: unknown, id: string, name: string) { return createServerReferenceImp(`${id}#${name}`, __callServer) diff --git a/spiceflow/src/react/server-dom-client-optimized.tsx b/spiceflow/src/react/server-dom-client-optimized.tsx index e52a82d..41d2e61 100644 --- a/spiceflow/src/react/server-dom-client-optimized.tsx +++ b/spiceflow/src/react/server-dom-client-optimized.tsx @@ -1,6 +1,4 @@ - import RSD from 'react-server-dom-vite/client' - -export const createServerReference = RSD.createServerReference; -export default RSD \ No newline at end of file +export const createServerReference = RSD.createServerReference +export default RSD diff --git a/spiceflow/src/react/server-dom-optimized.tsx b/spiceflow/src/react/server-dom-optimized.tsx index 7ae31f3..07a046d 100644 --- a/spiceflow/src/react/server-dom-optimized.tsx +++ b/spiceflow/src/react/server-dom-optimized.tsx @@ -1,7 +1,5 @@ - import RSD from 'react-server-dom-vite/server' - -export const registerServerReference = RSD.registerServerReference; -export const registerClientReference = RSD.registerClientReference; -export default RSD \ No newline at end of file +export const registerServerReference = RSD.registerServerReference +export const registerClientReference = RSD.registerClientReference +export default RSD diff --git a/spiceflow/src/react/types/ambient.d.ts b/spiceflow/src/react/types/ambient.d.ts index 0f6d1bc..37f080b 100644 --- a/spiceflow/src/react/types/ambient.d.ts +++ b/spiceflow/src/react/types/ambient.d.ts @@ -1,79 +1,78 @@ /// // import {Spiceflow} from '../../spiceflow.js' -declare module "react-server-dom-vite/server" { - export function renderToPipeableStream( - data: T, - manifest: import(".").ClientReferenceMetadataManifest, - options?: { - environmentName?: string | (() => string); - filterStackFrame?: (url: string, functionName: string) => boolean; - onError?: (error: any) => void; - onPostpone?: (reason: string) => void; - identifierPrefix?: string; - temporaryReferences?: any; - }, - ): import("react-dom/server").PipeableStream; +declare module 'react-server-dom-vite/server' { + export function renderToPipeableStream( + data: T, + manifest: import('.').ClientReferenceMetadataManifest, + options?: { + environmentName?: string | (() => string) + filterStackFrame?: (url: string, functionName: string) => boolean + onError?: (error: any) => void + onPostpone?: (reason: string) => void + identifierPrefix?: string + temporaryReferences?: any + }, + ): import('react-dom/server').PipeableStream - export function decodeReply(body: string | FormData): Promise; + export function decodeReply(body: string | FormData): Promise - export function decodeAction( - body: FormData, - manifest: import(".").ServerReferenceManifest, - ): Promise<() => Promise>; + export function decodeAction( + body: FormData, + manifest: import('.').ServerReferenceManifest, + ): Promise<() => Promise> - export function decodeFormState( - returnValue: unknown, - body: FormData, - manifest: import(".").ServerReferenceManifest, - ): Promise; + export function decodeFormState( + returnValue: unknown, + body: FormData, + manifest: import('.').ServerReferenceManifest, + ): Promise } -declare module "spiceflow/dist/react/server-dom-client-optimized" { - export function createFromNodeStream( - stream: import("node:stream").Readable, - manifest: import(".").ClientReferenceManifest, - ): Promise; +declare module 'spiceflow/dist/react/server-dom-client-optimized' { + export function createFromNodeStream( + stream: import('node:stream').Readable, + manifest: import('.').ClientReferenceManifest, + ): Promise - export function createFromReadableStream( - stream: ReadableStream, - manifest: import(".").ClientReferenceManifest, - options: { - callServer: import(".").CallServerFn; - }, - ): Promise; + export function createFromReadableStream( + stream: ReadableStream, + manifest: import('.').ClientReferenceManifest, + options: { + callServer: import('.').CallServerFn + }, + ): Promise - export function createFromFetch( - fetchReturn: ReturnType, - manifest: unknown, - options: { - callServer: import(".").CallServerFn; - }, - ): Promise; + export function createFromFetch( + fetchReturn: ReturnType, + manifest: unknown, + options: { + callServer: import('.').CallServerFn + }, + ): Promise - export function encodeReply(v: unknown[]): Promise; + export function encodeReply(v: unknown[]): Promise } -declare module "virtual:ssr-assets" { - export const bootstrapModules: string[]; +declare module 'virtual:ssr-assets' { + export const bootstrapModules: string[] } -declare module "virtual:app-entry" { - import type { Spiceflow } from "spiceflow"; - const app: Spiceflow; - export default app +declare module 'virtual:app-entry' { + import type { Spiceflow } from 'spiceflow' + const app: Spiceflow + export default app } -declare module "virtual:build-client-references" { - const value: Record Promise>>; - export default value; +declare module 'virtual:build-client-references' { + const value: Record Promise>> + export default value } -declare module "virtual:build-server-references" { - const value: Record Promise>>; - export default value; +declare module 'virtual:build-server-references' { + const value: Record Promise>> + export default value } - -declare const __raw_import: (id: string) => Promise; -declare const __callServer: CallServerFn; +declare const __raw_import: (id: string) => Promise +declare const __callServer: CallServerFn diff --git a/spiceflow/src/react/types/index.ts b/spiceflow/src/react/types/index.ts index 59edbc4..d7499e2 100644 --- a/spiceflow/src/react/types/index.ts +++ b/spiceflow/src/react/types/index.ts @@ -1,19 +1,19 @@ export type ClientReferenceMetadataManifest = { - resolveClientReferenceMetadata(metadata: { $$id: string }): string; -}; + resolveClientReferenceMetadata(metadata: { $$id: string }): string +} export type ClientReferenceManifest = { - resolveClientReference(reference: string): { - preload(): Promise; - get(): unknown; - }; -}; + resolveClientReference(reference: string): { + preload(): Promise + get(): unknown + } +} export type ServerReferenceManifest = { - resolveServerReference(reference: string): { - preload(): Promise; - get(): unknown; - }; -}; + resolveServerReference(reference: string): { + preload(): Promise + get(): unknown + } +} -export type CallServerFn = (id: string, args: unknown[]) => unknown; +export type CallServerFn = (id: string, args: unknown[]) => unknown diff --git a/spiceflow/src/react/utils/client-reference.ts b/spiceflow/src/react/utils/client-reference.ts index 467496f..36d7fb9 100644 --- a/spiceflow/src/react/utils/client-reference.ts +++ b/spiceflow/src/react/utils/client-reference.ts @@ -9,7 +9,7 @@ export const clientReferenceManifest: ClientReferenceManifest = { let mod: Record if (import.meta.env.DEV) { // console.log('importing client reference', id) - console.log('importing client reference', id) + console.log('importing client reference', id) mod = typeof __raw_import !== 'undefined' ? // on browser development need to use __raw_import to not add ?import at the end, otherwise the browser duplicates the module instance, context stops working @@ -19,10 +19,12 @@ export const clientReferenceManifest: ClientReferenceManifest = { const references = await import( 'virtual:build-client-references' as string ) - const ref = references.default[id] - if (!ref) { - throw new Error(`Can't find client reference for module ${id}, among ${Object.keys(references.default).join(', ')}`) - } + const ref = references.default[id] + if (!ref) { + throw new Error( + `Can't find client reference for module ${id}, among ${Object.keys(references.default).join(', ')}`, + ) + } mod = await ref() } resolved = mod[name] diff --git a/spiceflow/src/react/utils/fetch.ts b/spiceflow/src/react/utils/fetch.ts index 2d45071..299a5f3 100644 --- a/spiceflow/src/react/utils/fetch.ts +++ b/spiceflow/src/react/utils/fetch.ts @@ -1,77 +1,77 @@ -import type { IncomingMessage, ServerResponse } from "node:http"; -import { PassThrough, Readable } from "node:stream"; -import type { PipeableStream } from "react-dom/server"; +import type { IncomingMessage, ServerResponse } from 'node:http' +import { PassThrough, Readable } from 'node:stream' +import type { PipeableStream } from 'react-dom/server' export function createRequest( - req: IncomingMessage, - res: ServerResponse, + req: IncomingMessage, + res: ServerResponse, ): Request { - const abortController = new AbortController(); - res.once("close", () => { - if (req.destroyed) { - abortController.abort(); - } - }); + const abortController = new AbortController() + res.once('close', () => { + if (req.destroyed) { + abortController.abort() + } + }) - const headers = new Headers(); - for (const [k, v] of Object.entries(req.headers)) { - if (k.startsWith(":")) { - continue; - } - if (typeof v === "string") { - headers.set(k, v); - } else if (Array.isArray(v)) { - v.forEach((v) => headers.append(k, v)); - } - } + const headers = new Headers() + for (const [k, v] of Object.entries(req.headers)) { + if (k.startsWith(':')) { + continue + } + if (typeof v === 'string') { + headers.set(k, v) + } else if (Array.isArray(v)) { + v.forEach((v) => headers.append(k, v)) + } + } - return new Request( - new URL( - req.url || "/", - `${headers.get("x-forwarded-proto") ?? "http"}://${ - req.headers.host || "unknown.local" - }`, - ), - { - method: req.method, - body: - req.method === "GET" || req.method === "HEAD" - ? null - : (Readable.toWeb(req) as any), - headers, - signal: abortController.signal, - // @ts-ignore for undici - duplex: "half", - }, - ); + return new Request( + new URL( + req.url || '/', + `${headers.get('x-forwarded-proto') ?? 'http'}://${ + req.headers.host || 'unknown.local' + }`, + ), + { + method: req.method, + body: + req.method === 'GET' || req.method === 'HEAD' + ? null + : (Readable.toWeb(req) as any), + headers, + signal: abortController.signal, + // @ts-ignore for undici + duplex: 'half', + }, + ) } export function sendResponse(response: Response, res: ServerResponse) { - const headers = Object.fromEntries(response.headers); - if (headers["set-cookie"]) { - delete headers["set-cookie"]; - res.setHeader("set-cookie", response.headers.getSetCookie()); - } - res.writeHead(response.status, response.statusText, headers); + const headers = Object.fromEntries(response.headers) + if (headers['set-cookie']) { + delete headers['set-cookie'] + res.setHeader('set-cookie', response.headers.getSetCookie()) + } + res.writeHead(response.status, response.statusText, headers) - if (response.body) { - const abortController = new AbortController(); - res.once("close", () => abortController.abort()); - res.once("error", () => abortController.abort()); - Readable.fromWeb(response.body as any, { - signal: abortController.signal, - }).pipe(res); - } else { - res.end(); - } + if (response.body) { + const abortController = new AbortController() + res.once('close', () => abortController.abort()) + res.once('error', () => abortController.abort()) + Readable.fromWeb(response.body as any, { + signal: abortController.signal, + }).pipe(res) + } else { + res.end() + } } export function fromPipeableToWebReadable(stream: PipeableStream) { - return Readable.toWeb( - stream.pipe(new PassThrough()), - ) as ReadableStream; + return Readable.toWeb( + stream.pipe(new PassThrough()), + ) as ReadableStream } export function fromWebToNodeReadable(stream: ReadableStream) { - return Readable.fromWeb(stream as any); + return Readable.fromWeb(stream as any) } diff --git a/spiceflow/src/react/utils/normalize.ts b/spiceflow/src/react/utils/normalize.ts index ce94c91..c5e5ac0 100644 --- a/spiceflow/src/react/utils/normalize.ts +++ b/spiceflow/src/react/utils/normalize.ts @@ -8,7 +8,7 @@ import { ModuleNode, ViteDevServer } from 'vite' export function noramlizeClientReferenceId( id: string, parentServer: ViteDevServer, - mod?: ModuleNode + mod?: ModuleNode, ) { const root = parentServer.config.root if (id.startsWith(root)) { @@ -21,7 +21,7 @@ export function noramlizeClientReferenceId( // this is needed only for browser, so we'll strip it off // during ssr client reference import // TODO - + if (mod && mod.lastHMRTimestamp > 0) { id += `?t=${mod.lastHMRTimestamp}` } diff --git a/spiceflow/src/spiceflow.test.ts b/spiceflow/src/spiceflow.test.ts index da3502e..9342b6b 100644 --- a/spiceflow/src/spiceflow.test.ts +++ b/spiceflow/src/spiceflow.test.ts @@ -424,9 +424,8 @@ test('extractWildcardParam correctly extracts wildcard segments', () => { } `) - expect( - extractWildcardParam('/files/123/path/to/file.txt', '/files/:id/*'), - ).toMatchInlineSnapshot(` + expect(extractWildcardParam('/files/123/path/to/file.txt', '/files/:id/*')) + .toMatchInlineSnapshot(` { "*": "path/to/file.txt", } diff --git a/spiceflow/src/trie-router/node.ts b/spiceflow/src/trie-router/node.ts index fbdc7e6..6b9b820 100644 --- a/spiceflow/src/trie-router/node.ts +++ b/spiceflow/src/trie-router/node.ts @@ -1,6 +1,5 @@ -import { Pattern, splitRoutingPath, getPattern, splitPath } from "./url.js" -import { Params } from "./utils.js" - +import { Pattern, splitRoutingPath, getPattern, splitPath } from './url.js' +import { Params } from './utils.js' const METHOD_NAME_ALL = 'ALL' @@ -24,7 +23,11 @@ export class Node { #order: number = 0 #params: Record = emptyParams - constructor(method?: string, handler?: T, children?: Record>) { + constructor( + method?: string, + handler?: T, + children?: Record>, + ) { this.#children = children || Object.create(null) this.#methods = [] if (method && handler) { @@ -86,12 +89,13 @@ export class Node { node: Node, method: string, nodeParams: Record, - params?: Record + params?: Record, ): HandlerParamsSet[] { const handlerSets: HandlerParamsSet[] = [] for (let i = 0, len = node.#methods.length; i < len; i++) { const m = node.#methods[i] - const handlerSet = (m[method] || m[METHOD_NAME_ALL]) as HandlerParamsSet + const handlerSet = (m[method] || + m[METHOD_NAME_ALL]) as HandlerParamsSet const processedSet: Record = {} if (handlerSet !== undefined) { handlerSet.params = Object.create(null) @@ -101,7 +105,9 @@ export class Node { const key = handlerSet.possibleKeys[i] const processed = processedSet[handlerSet.score] handlerSet.params[key] = - params?.[key] && !processed ? params[key] : nodeParams[key] ?? params?.[key] + params?.[key] && !processed + ? params[key] + : (nodeParams[key] ?? params?.[key]) processedSet[handlerSet.score] = true } } @@ -135,10 +141,16 @@ export class Node { // '/hello/*' => match '/hello' if (nextNode.#children['*']) { handlerSets.push( - ...this.#getHandlerSets(nextNode.#children['*'], method, node.#params) + ...this.#getHandlerSets( + nextNode.#children['*'], + method, + node.#params, + ), ) } - handlerSets.push(...this.#getHandlerSets(nextNode, method, node.#params)) + handlerSets.push( + ...this.#getHandlerSets(nextNode, method, node.#params), + ) } else { tempNodes.push(nextNode) } @@ -153,7 +165,9 @@ export class Node { if (pattern === '*') { const astNode = node.#children['*'] if (astNode) { - handlerSets.push(...this.#getHandlerSets(astNode, method, node.#params)) + handlerSets.push( + ...this.#getHandlerSets(astNode, method, node.#params), + ) astNode.#params = params tempNodes.push(astNode) } @@ -174,7 +188,9 @@ export class Node { const m = matcher.exec(restPathString) if (m) { params[name] = m[0] - handlerSets.push(...this.#getHandlerSets(child, method, node.#params, params)) + handlerSets.push( + ...this.#getHandlerSets(child, method, node.#params, params), + ) if (Object.keys(child.#children).length) { child.#params = params @@ -190,10 +206,17 @@ export class Node { if (matcher === true || matcher.test(part)) { params[name] = part if (isLast) { - handlerSets.push(...this.#getHandlerSets(child, method, params, node.#params)) + handlerSets.push( + ...this.#getHandlerSets(child, method, params, node.#params), + ) if (child.#children['*']) { handlerSets.push( - ...this.#getHandlerSets(child.#children['*'], method, params, node.#params) + ...this.#getHandlerSets( + child.#children['*'], + method, + params, + node.#params, + ), ) } } else { @@ -213,6 +236,10 @@ export class Node { }) } - return [handlerSets.map(({ handler, params }) => [handler, params] as [T, Params])] + return [ + handlerSets.map( + ({ handler, params }) => [handler, params] as [T, Params], + ), + ] } } diff --git a/spiceflow/src/trie-router/router.ts b/spiceflow/src/trie-router/router.ts index 87eef7f..42f675a 100644 --- a/spiceflow/src/trie-router/router.ts +++ b/spiceflow/src/trie-router/router.ts @@ -1,5 +1,3 @@ - - import { Node } from './node.js' import { checkOptionalParameter, Result } from './utils.js' diff --git a/spiceflow/src/trie-router/url.ts b/spiceflow/src/trie-router/url.ts index e1c31f0..bc0e4ab 100644 --- a/spiceflow/src/trie-router/url.ts +++ b/spiceflow/src/trie-router/url.ts @@ -20,7 +20,9 @@ export const splitRoutingPath = (routePath: string): string[] => { return replaceGroupMarks(paths, groups) } -const extractGroupsFromPath = (path: string): { groups: [string, string][]; path: string } => { +const extractGroupsFromPath = ( + path: string, +): { groups: [string, string][]; path: string } => { const groups: [string, string][] = [] path = path.replace(/\{[^}]+\}/g, (match, index) => { @@ -32,7 +34,10 @@ const extractGroupsFromPath = (path: string): { groups: [string, string][]; path return { groups, path } } -const replaceGroupMarks = (paths: string[], groups: [string, string][]): string[] => { +const replaceGroupMarks = ( + paths: string[], + groups: [string, string][], +): string[] => { for (let i = groups.length - 1; i >= 0; i--) { const [mark] = groups[i] @@ -115,7 +120,9 @@ export const getPath = (request: Request): string => { // Although this is a performance disadvantage, it is acceptable since we prefer cases that do not include percent encoding. const queryIndex = url.indexOf('?', i) const path = url.slice(start, queryIndex === -1 ? undefined : queryIndex) - return tryDecodeURI(path.includes('%25') ? path.replace(/%25/g, '%2525') : path) + return tryDecodeURI( + path.includes('%25') ? path.replace(/%25/g, '%2525') : path, + ) } else if (charCode === 63) { // '?' break @@ -133,7 +140,9 @@ export const getPathNoStrict = (request: Request): string => { const result = getPath(request) // if strict routing is false => `/hello/hey/` and `/hello/hey` are treated the same - return result.length > 1 && result.at(-1) === '/' ? result.slice(0, -1) : result + return result.length > 1 && result.at(-1) === '/' + ? result.slice(0, -1) + : result } export const mergePath = (...paths: string[]): string => { @@ -219,8 +228,13 @@ const _decodeURI = (value: string) => { const _getQueryParam = ( url: string, key?: string, - multiple?: boolean -): string | undefined | Record | string[] | Record => { + multiple?: boolean, +): + | string + | undefined + | Record + | string[] + | Record => { let encoded if (!multiple && key && !/[%+]/.test(key)) { @@ -235,7 +249,9 @@ const _getQueryParam = ( if (trailingKeyCode === 61) { const valueIndex = keyIndex + key.length + 2 const endIndex = url.indexOf('&', valueIndex) - return _decodeURI(url.slice(valueIndex, endIndex === -1 ? undefined : endIndex)) + return _decodeURI( + url.slice(valueIndex, endIndex === -1 ? undefined : endIndex), + ) } else if (trailingKeyCode == 38 || isNaN(trailingKeyCode)) { return '' } @@ -261,7 +277,11 @@ const _getQueryParam = ( } let name = url.slice( keyIndex + 1, - valueIndex === -1 ? (nextKeyIndex === -1 ? undefined : nextKeyIndex) : valueIndex + valueIndex === -1 + ? nextKeyIndex === -1 + ? undefined + : nextKeyIndex + : valueIndex, ) if (encoded) { name = _decodeURI(name) @@ -277,7 +297,10 @@ const _getQueryParam = ( if (valueIndex === -1) { value = '' } else { - value = url.slice(valueIndex + 1, nextKeyIndex === -1 ? undefined : nextKeyIndex) + value = url.slice( + valueIndex + 1, + nextKeyIndex === -1 ? undefined : nextKeyIndex, + ) if (encoded) { value = _decodeURI(value) } @@ -298,19 +321,22 @@ const _getQueryParam = ( export const getQueryParam: ( url: string, - key?: string + key?: string, ) => string | undefined | Record = _getQueryParam as ( url: string, - key?: string + key?: string, ) => string | undefined | Record export const getQueryParams = ( url: string, - key?: string + key?: string, ): string[] | undefined | Record => { - return _getQueryParam(url, key, true) as string[] | undefined | Record + return _getQueryParam(url, key, true) as + | string[] + | undefined + | Record } // `decodeURIComponent` is a long name. // By making it a function, we can use it commonly when minified, reducing the amount of code. -export const decodeURIComponent_ = decodeURIComponent \ No newline at end of file +export const decodeURIComponent_ = decodeURIComponent diff --git a/spiceflow/src/trie-router/utils.ts b/spiceflow/src/trie-router/utils.ts index 7081aae..aaacc47 100644 --- a/spiceflow/src/trie-router/utils.ts +++ b/spiceflow/src/trie-router/utils.ts @@ -54,7 +54,7 @@ export type Params = Record * An array of handlers with their corresponding parameter maps. * * Example: - * + * * [[handler, params][]] * ```typescript * [ @@ -67,4 +67,3 @@ export type Params = Record * ``` */ export type Result = [[T, Params][]] - diff --git a/spiceflow/src/types.ts b/spiceflow/src/types.ts index b75b028..2bcf6cb 100644 --- a/spiceflow/src/types.ts +++ b/spiceflow/src/types.ts @@ -1,7 +1,6 @@ // https://github.com/remorses/elysia/blob/main/src/types.ts#L6 import Ajv, { ValidateFunction } from 'ajv' - import z from 'zod' import type { @@ -33,8 +32,8 @@ export type ObjectValues = T[keyof T] type IsPathParameter = Part extends `:${infer Parameter}` ? Parameter : Part extends `*` - ? '*' - : never + ? '*' + : never export type GetPathParameter = Path extends `${infer A}/${infer B}` @@ -70,15 +69,16 @@ export type NeverKey = { [K in keyof T]?: T[K] } & {} -type IsBothObject = A extends Record - ? B extends Record - ? IsClass extends false - ? IsClass extends false - ? true +type IsBothObject = + A extends Record + ? B extends Record + ? IsClass extends false + ? IsClass extends false + ? true + : false : false : false : false - : false type IsClass = V extends abstract new (...args: any) => any ? true : false @@ -91,57 +91,57 @@ export type Reconcile< > = Stack['length'] extends 16 ? A : Override extends true - ? { - [key in keyof A as key extends keyof B ? never : key]: A[key] - } extends infer Collision - ? {} extends Collision - ? { - [key in keyof B]: IsBothObject< - // @ts-ignore trust me bro - A[key], - B[key] - > extends true - ? Reconcile< - // @ts-ignore trust me bro - A[key], - B[key], - Override, - [0, ...Stack] - > - : B[key] - } - : Prettify< - Collision & { - [key in keyof B]: B[key] - } - > - : never - : { - [key in keyof B as key extends keyof A ? never : key]: B[key] - } extends infer Collision - ? {} extends Collision ? { - [key in keyof A]: IsBothObject< - A[key], - // @ts-ignore trust me bro - B[key] - > extends true - ? Reconcile< + [key in keyof A as key extends keyof B ? never : key]: A[key] + } extends infer Collision + ? {} extends Collision + ? { + [key in keyof B]: IsBothObject< // @ts-ignore trust me bro + A[key], + B[key] + > extends true + ? Reconcile< + // @ts-ignore trust me bro + A[key], + B[key], + Override, + [0, ...Stack] + > + : B[key] + } + : Prettify< + Collision & { + [key in keyof B]: B[key] + } + > + : never + : { + [key in keyof B as key extends keyof A ? never : key]: B[key] + } extends infer Collision + ? {} extends Collision + ? { + [key in keyof A]: IsBothObject< A[key], // @ts-ignore trust me bro - B[key], - Override, - [0, ...Stack] - > - : A[key] - } - : Prettify< - { - [key in keyof A]: A[key] - } & Collision - > - : never + B[key] + > extends true + ? Reconcile< + // @ts-ignore trust me bro + A[key], + // @ts-ignore trust me bro + B[key], + Override, + [0, ...Stack] + > + : A[key] + } + : Prettify< + { + [key in keyof A]: A[key] + } & Collision + > + : never export interface SingletonBase { state: Record @@ -181,16 +181,16 @@ export type UnwrapSchema< > = undefined extends Schema ? unknown : Schema extends ZodTypeAny - ? z.infer - : Schema extends TSchema - ? Schema extends OptionalField - ? Prettify>> - : StaticDecode - : Schema extends string - ? Definitions extends Record - ? NamedSchema - : Definitions - : unknown + ? z.infer + : Schema extends TSchema + ? Schema extends OptionalField + ? Prettify>> + : StaticDecode + : Schema extends string + ? Definitions extends Record + ? NamedSchema + : Definitions + : unknown export interface UnwrapRoute< in out Schema extends InputSchema, @@ -204,13 +204,13 @@ export interface UnwrapRoute< 200: UnwrapSchema } : Schema['response'] extends Record - ? { - [k in keyof Schema['response']]: UnwrapSchema< - Schema['response'][k], - Definitions - > - } - : unknown | void + ? { + [k in keyof Schema['response']]: UnwrapSchema< + Schema['response'][k], + Definitions + > + } + : unknown | void } export type LifeCycleEvent = @@ -299,8 +299,8 @@ export interface MergeSchema< ? {} : B['response'] : {} extends B['response'] - ? A['response'] - : A['response'] & Omit + ? A['response'] + : A['response'] & Omit } export type Handler< @@ -317,29 +317,31 @@ export type Handler< : Route['response'][keyof Route['response']] > -export type Replace = IsAny extends true - ? Original - : Original extends Record - ? { - [K in keyof Original]: Original[K] extends Target ? With : Original[K] - } - : Original extends Target - ? With - : Original +export type Replace = + IsAny extends true + ? Original + : Original extends Record + ? { + [K in keyof Original]: Original[K] extends Target ? With : Original[K] + } + : Original extends Target + ? With + : Original export type IsAny = 0 extends 1 & T ? true : false -export type CoExist = IsAny extends true - ? Original - : Original extends Record - ? { - [K in keyof Original]: Original[K] extends Target - ? Original[K] | With - : Original[K] - } - : Original extends Target - ? Original | With - : Original +export type CoExist = + IsAny extends true + ? Original + : Original extends Record + ? { + [K in keyof Original]: Original[K] extends Target + ? Original[K] | With + : Original[K] + } + : Original extends Target + ? Original | With + : Original export type InlineHandler< Route extends RouteSchema = {}, @@ -376,11 +378,12 @@ export type OptionalHandler< state: {} }, Path extends string = '', -> = Handler extends ( - context: infer Context, -) => infer Returned - ? (context: Context) => Returned | MaybePromise - : never +> = + Handler extends ( + context: infer Context, + ) => infer Returned + ? (context: Context) => Returned | MaybePromise + : never export type AfterHandler< in out Route extends RouteSchema = {}, @@ -388,17 +391,18 @@ export type AfterHandler< state: {} }, Path extends string = '', -> = Handler extends ( - context: infer Context, -) => infer Returned - ? ( - context: Prettify< - { - response: Route['response'] - } & Context - >, - ) => Returned | MaybePromise - : never +> = + Handler extends ( + context: infer Context, + ) => infer Returned + ? ( + context: Prettify< + { + response: Route['response'] + } & Context + >, + ) => Returned | MaybePromise + : never export type MapResponse< in out Route extends RouteSchema = {}, @@ -643,8 +647,8 @@ export type CreateClient< > = Path extends `/${infer Rest}` ? _CreateClient : Path extends '' - ? _CreateClient<'index', Property> - : _CreateClient + ? _CreateClient<'index', Property> + : _CreateClient export type ComposeSpiceflowResponse = Handle extends ( ...a: any[] @@ -880,11 +884,10 @@ export type HTTPHeaders = Record & { export type JoinPath = `${A}${B extends '/' ? '/index' : B extends '' - ? B - : B extends `/${string}` - ? B - : B}` + ? B + : B extends `/${string}` + ? B + : B}` export type PartialWithRequired = Partial> & Pick - diff --git a/spiceflow/src/vite-jacob.ts b/spiceflow/src/vite-jacob.ts index d24d26c..5386b14 100644 --- a/spiceflow/src/vite-jacob.ts +++ b/spiceflow/src/vite-jacob.ts @@ -39,9 +39,7 @@ export default function reactServerDOM(): vite.PluginOption { return noramlizeClientReferenceId(filename, server) } - return [ - - ] + return [] } function rollupInputsToArray( From 581517857364dadb12ee30ba527c828a6a8bfb92 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 16:49:19 +0100 Subject: [PATCH 043/100] put rsc entry inside spiceflow --- spiceflow/src/react/entry.rsc.tsx | 105 ++---------------------- spiceflow/src/react/entry.ssr.tsx | 17 ++-- spiceflow/src/spiceflow.ts | 131 ++++++++++++++++++++++++++---- 3 files changed, 128 insertions(+), 125 deletions(-) diff --git a/spiceflow/src/react/entry.rsc.tsx b/spiceflow/src/react/entry.rsc.tsx index 10437c3..6b4a25c 100644 --- a/spiceflow/src/react/entry.rsc.tsx +++ b/spiceflow/src/react/entry.rsc.tsx @@ -14,105 +14,10 @@ export interface RscHandlerResult { stream: ReadableStream } -export interface ServerPayload { - root: FlightData - formState?: ReactFormState - returnValue?: unknown -} - export async function handler(url: URL, request: Request) { // handle action - let returnValue: unknown | undefined - let formState: ReactFormState | undefined - if (request.method === 'POST') { - const actionId = url.searchParams.get('__rsc') - if (actionId) { - // client stream request - const contentType = request.headers.get('content-type') - const body = contentType?.startsWith('multipart/form-data') - ? await request.formData() - : await request.text() - const args = await ReactServer.decodeReply(body) - const reference = serverReferenceManifest.resolveServerReference(actionId) - await reference.preload() - const action = await reference.get() - returnValue = await (action as any).apply(null, args) - } else { - // progressive enhancement - const formData = await request.formData() - console.log(formData) - const decodedAction = await ReactServer.decodeAction( - formData, - serverReferenceManifest, - ) - formState = await ReactServer.decodeFormState( - await decodedAction(), - formData, - serverReferenceManifest, - ) - } - } - - const root = await app.handle(request) - - if (root instanceof Response) { - return root - } - const { page, layouts } = root - - let abortable = ReactServer.renderToPipeableStream( - { - root, - returnValue, - formState, - }, - clientReferenceMetadataManifest, - { onError(error) {} }, - ) - // render flight stream - const stream = fromPipeableToWebReadable(abortable) - request.signal.addEventListener('abort', () => { - abortable.abort() - }) - - let r: RscHandlerResult = { - stream, - } - return r -} - -const serverReferenceManifest: ServerReferenceManifest = { - resolveServerReference(reference: string) { - const [id, name] = reference.split('#') - let resolved: unknown - return { - async preload() { - let mod: Record - if (import.meta.env.DEV) { - mod = await import(/* @vite-ignore */ id) - } else { - const references = await import('virtual:build-server-references') - const ref = references.default[id] - if (!ref) { - const availableKeys = Object.keys(references.default) - throw new Error( - `Could not find server reference for id: ${id}. This likely means the server reference was not properly registered. Available reference keys are: ${availableKeys.join(', ')}`, - ) - } - mod = await ref() - } - resolved = mod[name] - }, - get() { - return resolved - }, - } - }, -} - -const clientReferenceMetadataManifest: ClientReferenceMetadataManifest = { - resolveClientReferenceMetadata(metadata) { - // console.log("[debug:resolveClientReferenceMetadata]", { metadata }, Object.getOwnPropertyDescriptors(metadata)); - return metadata.$$id - }, -} + + const response = await app.handle(request) + + return response +} \ No newline at end of file diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 1dc4d1a..6731766 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -23,24 +23,19 @@ export default async function handler( const request = createRequest(req, res) const url = new URL(request.url) const rscEntry = await importRscEntry() - const rscResult = await rscEntry.handler(url, request) + const response = await rscEntry.handler(url, request) - if (rscResult instanceof Response) { - sendResponse(rscResult, res) + if (!response.headers.get('content-type')?.startsWith('text/x-component')) { + sendResponse(response, res) return } if (url.searchParams.has('__rsc')) { - const response = new Response(rscResult.stream, { - headers: { - 'content-type': 'text/x-component;charset=utf-8', - }, - }) sendResponse(response, res) return } - const [flightStream1, flightStream2] = rscResult.stream.tee() + const [flightStream1, flightStream2] = response.body!.tee() const payload = await ReactClient.createFromNodeStream( fromWebToNodeReadable(flightStream1), @@ -66,7 +61,7 @@ export default async function handler( }), ) - const response = new Response( + const htmlResponse = new Response( htmlStream.pipeThrough(injectRSCPayload(flightStream2)), { headers: { @@ -74,7 +69,7 @@ export default async function handler( }, }, ) - sendResponse(response, res) + sendResponse(htmlResponse, res) } declare let __rscRunner: ModuleRunner diff --git a/spiceflow/src/spiceflow.ts b/spiceflow/src/spiceflow.ts index af2bfe3..32ab0ed 100644 --- a/spiceflow/src/spiceflow.ts +++ b/spiceflow/src/spiceflow.ts @@ -1,14 +1,15 @@ +import type { ReactFormState } from 'react-dom/client' +import ReactServer from 'spiceflow/dist/react/server-dom-optimized' + import addFormats from 'ajv-formats' import lodashCloneDeep from 'lodash.clonedeep' import superjson from 'superjson' import { ComposeSpiceflowResponse, - ContentType, CreateClient, DefinitionBase, ErrorHandler, - HTTPMethod, InlineHandler, InputSchema, InternalRoute, @@ -25,29 +26,27 @@ import { RouteSchema, SingletonBase, TypeSchema, - UnwrapRoute, + UnwrapRoute } from './types.js' let globalIndex = 0 import Ajv, { ValidateFunction } from 'ajv' +import { createElement } from 'react' import { z, ZodType } from 'zod' import { zodToJsonSchema } from 'zod-to-json-schema' -import { Context, MiddlewareContext } from './context.js' +import { MiddlewareContext } from './context.js' import { isProduction, ValidationError } from './error.js' import { isAsyncIterable, isResponse, redirect } from './utils.js' -import { createElement, isValidElement } from 'react' -import value from 'virtual:build-client-references' + import { FlightData, LayoutContent } from './react/components.js' +import { ClientReferenceMetadataManifest, ServerReferenceManifest } from './react/types/index.js' +import { fromPipeableToWebReadable } from './react/utils/fetch.js' import { TrieRouter } from './trie-router/router.js' +import { decodeURIComponent_ } from './trie-router/url.js' import { - ParamIndexMap, - Params, - ParamStash, - Result, + Result } from './trie-router/utils.js' -import { decodeURIComponent_, tryDecode } from './trie-router/url.js' -import path from 'path' const ajv = (addFormats.default || addFormats)( new (Ajv.default || Ajv)({ useDefaults: true }), @@ -905,12 +904,71 @@ export class Spiceflow< }), ]) - let data: FlightData = { + let root: FlightData = { url: request.url, page, layouts, } - return data + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + if (request.method === 'POST') { + const url = new URL(request.url) + const actionId = url.searchParams.get('__rsc') + if (actionId) { + // client stream request + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + const args = await ReactServer.decodeReply(body) + const reference = + serverReferenceManifest.resolveServerReference(actionId) + await reference.preload() + const action = await reference.get() + returnValue = await (action as any).apply(null, args) + } else { + // progressive enhancement + const formData = await request.formData() + console.log(formData) + const decodedAction = await ReactServer.decodeAction( + formData, + serverReferenceManifest, + ) + formState = await ReactServer.decodeFormState( + await decodedAction(), + formData, + serverReferenceManifest, + ) + } + } + + + + if (root instanceof Response) { + return root + } + + + let abortable = ReactServer.renderToPipeableStream( + { + root, + returnValue, + formState, + }, + clientReferenceMetadataManifest, + { onError(error) {} }, + ) + // render flight stream + const stream = fromPipeableToWebReadable(abortable) + request.signal.addEventListener('abort', () => { + abortable.abort() + }) + + return new Response(stream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8' + } + }) } catch (err) { return await getResForError(err) } @@ -1617,3 +1675,48 @@ function partition(arr: T[], predicate: (item: T) => boolean): [T[], T[]] { [[], []] as [T[], T[]], ) } + + + +const serverReferenceManifest: ServerReferenceManifest = { + resolveServerReference(reference: string) { + const [id, name] = reference.split('#') + let resolved: unknown + return { + async preload() { + let mod: Record + if (import.meta.env.DEV) { + mod = await import(/* @vite-ignore */ id) + } else { + const references = await import('virtual:build-server-references') + const ref = references.default[id] + if (!ref) { + const availableKeys = Object.keys(references.default) + throw new Error( + `Could not find server reference for id: ${id}. This likely means the server reference was not properly registered. Available reference keys are: ${availableKeys.join(', ')}`, + ) + } + mod = await ref() + } + resolved = mod[name] + }, + get() { + return resolved + }, + } + }, +} + +const clientReferenceMetadataManifest: ClientReferenceMetadataManifest = { + resolveClientReferenceMetadata(metadata) { + // console.log("[debug:resolveClientReferenceMetadata]", { metadata }, Object.getOwnPropertyDescriptors(metadata)); + return metadata.$$id + }, +} + + +export interface ServerPayload { + root: FlightData + formState?: ReactFormState + returnValue?: unknown +} From c74c6af245e10238a2a0712e3c82091b4b4a69a3 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 16:52:55 +0100 Subject: [PATCH 044/100] fix tsc errors --- spiceflow/src/react/components.tsx | 2 - spiceflow/src/react/entry.client.tsx | 3 +- spiceflow/src/react/entry.ssr.tsx | 3 +- spiceflow/src/react/types/ambient.d.ts | 13 +++ spiceflow/src/vite-jacob.ts | 114 ------------------------- spiceflow/src/vite.tsx | 18 ++-- 6 files changed, 23 insertions(+), 130 deletions(-) delete mode 100644 spiceflow/src/vite-jacob.ts diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 8bd2caf..99660ef 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -1,9 +1,7 @@ 'use client' import React from 'react' -import { RouteMatch } from '../router.js' import { ReactFormState } from 'react-dom/client' -import { InternalRoute } from '../types.js' export const FlightDataContext = React.createContext(undefined!) // Get $$id property that was set by registerClientReference diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 2979e7e..50bdd27 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,11 +1,12 @@ import React from 'react' import ReactDomClient from 'react-dom/client' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' -import type { ServerPayload } from './entry.rsc.js' + import type { CallServerFn } from './types/index.js' import { clientReferenceManifest } from './utils/client-reference.js' import { rscStream } from 'rsc-html-stream/client' import { FlightDataContext } from './components.js' +import { ServerPayload } from '../spiceflow.js' async function main() { const callServer: CallServerFn = async (id, args) => { diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 6731766..871fc50 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http' import ReactDomServer from 'react-dom/server' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' import type { ModuleRunner } from 'vite/module-runner' -import type { ServerPayload } from './entry.rsc.js' + import { createRequest, @@ -15,6 +15,7 @@ import { FlightDataContext } from './components.js' import { bootstrapModules } from 'virtual:ssr-assets' import { clientReferenceManifest } from './utils/client-reference.js' import cssUrls from 'virtual:app-styles' +import { ServerPayload } from '../spiceflow.js' export default async function handler( req: IncomingMessage, diff --git a/spiceflow/src/react/types/ambient.d.ts b/spiceflow/src/react/types/ambient.d.ts index 37f080b..e8f2fed 100644 --- a/spiceflow/src/react/types/ambient.d.ts +++ b/spiceflow/src/react/types/ambient.d.ts @@ -27,6 +27,15 @@ declare module 'react-server-dom-vite/server' { body: FormData, manifest: import('.').ServerReferenceManifest, ): Promise + const defaultExport: { + registerServerReference: Function + registerClientReference: Function + decodeReply: decodeReply + decodeAction: decodeAction + decodeFormState: decodeFormState + renderToPipeableStream: renderToPipeableStream + } + export default defaultExport } declare module 'spiceflow/dist/react/server-dom-client-optimized' { @@ -57,6 +66,10 @@ declare module 'spiceflow/dist/react/server-dom-client-optimized' { declare module 'virtual:ssr-assets' { export const bootstrapModules: string[] } +declare module 'virtual:app-styles' { + const cssUrls: string[] + export default cssUrls +} declare module 'virtual:app-entry' { import type { Spiceflow } from 'spiceflow' diff --git a/spiceflow/src/vite-jacob.ts b/spiceflow/src/vite-jacob.ts deleted file mode 100644 index 5386b14..0000000 --- a/spiceflow/src/vite-jacob.ts +++ /dev/null @@ -1,114 +0,0 @@ -import * as crypto from 'node:crypto' -import * as fs from 'node:fs' -import * as path from 'node:path' -import react, { type Options as ReactOptions } from '@vitejs/plugin-react' -import { clientTransform, serverTransform } from 'unplugin-rsc' -import * as vite from 'vite' -import { debugTransformResult } from './vite.js' -import { noramlizeClientReferenceId } from './react/utils/normalize.js' - -export default function reactServerDOM(): vite.PluginOption { - let env: vite.ConfigEnv - const serverEnvironments = new Set(['rsc']) - const ssrEnvironments = new Set(['ssr']) - const browserEnvironment = 'client' - - const clientEntries = new Set() - const clientModules = new Map() - const serverModules = new Map() - let browserOutput: vite.Rollup.RollupOutput | undefined - - let server: vite.ViteDevServer - let generateId = (filename, directive) => { - if (env.command === 'build') { - const hash = crypto - .createHash('sha256') - .update(filename) - .digest('hex') - .slice(0, 8) - - if (directive === 'use server') { - serverModules.set(filename, hash) - return hash - } - - clientModules.set(filename, hash) - return hash - } - - return noramlizeClientReferenceId(filename, server) - } - - return [] -} - -function rollupInputsToArray( - rollupInputs: vite.Rollup.InputOption | undefined, -) { - return Array.isArray(rollupInputs) - ? rollupInputs - : typeof rollupInputs === 'string' - ? [rollupInputs] - : rollupInputs - ? Object.values(rollupInputs) - : [] -} - -function collectChunks( - base: string, - forFilename: string, - manifest: Record, - collected: Set = new Set(), -) { - if (manifest[forFilename]) { - collected.add(base + manifest[forFilename].file) - for (const imp of manifest[forFilename].imports ?? []) { - collectChunks(base, imp, manifest, collected) - } - } - - return Array.from(collected) -} - -function moveStaticAssets( - output: vite.Rollup.RollupOutput, - outDir: string, - clientOutDir: string, -) { - const manifestAsset = output.output.find( - (asset) => asset.fileName === '.vite/ssr-manifest.json', - ) - if (!manifestAsset || manifestAsset.type !== 'asset') - throw new Error('could not find manifest') - const manifest = JSON.parse(manifestAsset.source as string) - - const processed = new Set() - for (const assets of Object.values(manifest) as string[][]) { - for (const asset of assets) { - const fullPath = path.join(outDir, asset.slice(1)) - - if (asset.endsWith('.js') || processed.has(fullPath)) continue - processed.add(fullPath) - - if (!fs.existsSync(fullPath)) continue - - const relative = path.relative(outDir, fullPath) - fs.renameSync(fullPath, path.join(clientOutDir, relative)) - } - } -} - -const EXTENSIONS_TO_TRANSFORM = new Set([ - '.js', - '.jsx', - '.cjs', - '.cjsx', - '.mjs', - '.mjsx', - '.ts', - '.tsx', - '.cts', - '.ctsx', - '.mts', - '.mtsx', -]) diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 2f88b4c..c67e70b 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -1,28 +1,22 @@ import assert from 'node:assert' import * as vite from 'vite' +import react from '@vitejs/plugin-react' +import crypto from 'node:crypto' import fs from 'node:fs' -import url from 'node:url' import path from 'node:path' -import react from '@vitejs/plugin-react' +import url, { fileURLToPath } from 'node:url' +import { clientTransform, serverTransform } from 'unplugin-rsc' import { type Manifest, type Plugin, PluginOption, type RunnableDevEnvironment, - UserConfig, ViteDevServer, - createRunnableDevEnvironment, - createServerModuleRunner, - defineConfig, + createRunnableDevEnvironment } from 'vite' -import { fileURLToPath } from 'node:url' -import crypto from 'node:crypto' -import reactServerDOM from './vite-jacob.js' -import { serverTransform, clientTransform } from 'unplugin-rsc' -import { noramlizeClientReferenceId } from './react/utils/normalize.js' import { collectStyleUrls } from './react/css.js' -import { normalizeId } from 'ajv/dist/compile/resolve.js' +import { noramlizeClientReferenceId } from './react/utils/normalize.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) From 7cdecea01fc1df4db05b19e2ae4419ae31bab454 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 17:39:02 +0100 Subject: [PATCH 045/100] adding a bit of error handling in ssr and client --- spiceflow/src/react/components.tsx | 125 +++++++++++++++++++++++++-- spiceflow/src/react/entry.client.tsx | 18 ++-- spiceflow/src/react/entry.ssr.tsx | 54 +++++++++--- 3 files changed, 175 insertions(+), 22 deletions(-) diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 99660ef..2b06c04 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -1,6 +1,6 @@ 'use client' -import React from 'react' +import React, { Suspense } from 'react' import { ReactFormState } from 'react-dom/client' export const FlightDataContext = React.createContext(undefined!) @@ -13,13 +13,18 @@ export function useFlightData() { export function LayoutContent(props: { id: string }) { const data = useFlightData() - const layoutIndex = data.layouts.findIndex((layout) => layout.id === props.id) - let nextLayout = data.layouts[layoutIndex + 1]?.element - if (nextLayout) { - return nextLayout - } + const elem = (() => { + const layoutIndex = data.layouts.findIndex( + (layout) => layout.id === props.id, + ) + let nextLayout = data.layouts[layoutIndex + 1]?.element + if (nextLayout) { + return nextLayout + } - return data.page + return data.page + })() + return elem } export type FlightData = { @@ -43,3 +48,109 @@ interface ReactServerErrorContext { status: number headers?: Record } + +export interface ErrorPageProps { + error: Error + serverError?: ReactServerErrorContext + reset: () => void +} + +interface Props { + children?: React.ReactNode + errorComponent: React.FC +} + +interface State { + error: Error | null +} + +function isRedirectError(ctx: ReactServerErrorContext) { + return ctx.status >= 300 && ctx.status < 400 +} + +function isNotFoundError(ctx: ReactServerErrorContext) { + return ctx.status === 404 +} + +function getErrorContext(error: Error): ReactServerErrorContext | undefined { + return (error as any).serverError +} + +export class ErrorBoundary extends React.Component { + constructor(props: Props) { + super(props) + this.state = { error: null } + } + + static getDerivedStateFromError(error: Error) { + const ctx = getErrorContext(error) + if (ctx && (isNotFoundError(ctx) || isRedirectError(ctx))) { + throw error + } + return { error } + } + + reset = () => { + React.startTransition(() => { + this.setState({ error: null }) + }) + } + + override render() { + const error = this.state.error + if (error) { + return ( + <> + + + + ) + } + return this.props.children + } +} + +function ErrorAutoReset(props: Pick) { + // TODO + // const href = useRouter((s) => s.location.href); + // const initialHref = React.useRef(href).current; + // React.useEffect(() => { + // if (href !== initialHref) { + // props.reset(); + // } + // }, [href]); + return null +} + +// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/build/webpack/loaders/next-app-loader.ts#L73 +// https://github.com/vercel/next.js/blob/677c9b372faef680d17e9ba224743f44e1107661/packages/next/src/client/components/error-boundary.tsx#L145 +export function DefaultGlobalErrorPage(props: ErrorPageProps) { + const message = props.serverError + ? `Unknown Server Error (see server logs for the details)` + : `Unknown Client Error (see browser console for the details)` + return ( + + {message} + +

{message}

+ + + ) +} diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 50bdd27..0527c67 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,11 +1,15 @@ -import React from 'react' +import React, { Suspense } from 'react' import ReactDomClient from 'react-dom/client' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' import type { CallServerFn } from './types/index.js' import { clientReferenceManifest } from './utils/client-reference.js' import { rscStream } from 'rsc-html-stream/client' -import { FlightDataContext } from './components.js' +import { + DefaultGlobalErrorPage, + ErrorBoundary, + FlightDataContext, +} from './components.js' import { ServerPayload } from '../spiceflow.js' async function main() { @@ -60,9 +64,13 @@ async function main() { }, []) return ( - - {payload.root?.layouts?.[0]?.element ?? payload.root.page} - + Loading root...}> + + + {payload.root?.layouts?.[0]?.element ?? payload.root.page} + + + ) } diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 871fc50..cf0959e 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -3,7 +3,6 @@ import ReactDomServer from 'react-dom/server' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' import type { ModuleRunner } from 'vite/module-runner' - import { createRequest, fromPipeableToWebReadable, @@ -11,11 +10,16 @@ import { sendResponse, } from './utils/fetch.js' import { injectRSCPayload } from 'rsc-html-stream/server' -import { FlightDataContext } from './components.js' +import { + DefaultGlobalErrorPage, + ErrorBoundary, + FlightDataContext, +} from './components.js' import { bootstrapModules } from 'virtual:ssr-assets' import { clientReferenceManifest } from './utils/client-reference.js' import cssUrls from 'virtual:app-styles' import { ServerPayload } from '../spiceflow.js' +import { Suspense } from 'react' export default async function handler( req: IncomingMessage, @@ -43,28 +47,58 @@ export default async function handler( clientReferenceManifest, ) const ssrAssets = await import('virtual:ssr-assets') - const el = ( {cssUrls.map((url) => ( - + // precedence to force head rendering + // https://react.dev/reference/react-dom/components/link#special-rendering-behavior + ))} {payload.root?.layouts?.[0]?.element ?? payload.root.page} ) - const htmlStream = fromPipeableToWebReadable( - ReactDomServer.renderToPipeableStream(el, { - bootstrapModules: ssrAssets.bootstrapModules, + let htmlStream: ReadableStream + let status = 200 - // @ts-ignore no type? + try { + const ssrStream = await ReactDomServer.renderToPipeableStream(el, { + bootstrapModules: ssrAssets.bootstrapModules, + // @ts-ignore formState: payload.formState, - }), - ) + onError(error) { + console.error('[react-dom:renderToPipeableStream]', error) + status = 500 + }, + }) + + htmlStream = fromPipeableToWebReadable(ssrStream) + } catch (e) { + console.log(`error during ssr render catch`, e) + // On error, render minimal HTML shell + // Client will do full CSR render and show error boundary + status = 500 + const errorRoot = ( + + + + + + + + + ) + + const errorStream = ReactDomServer.renderToPipeableStream(errorRoot, { + bootstrapModules: ssrAssets.bootstrapModules, + }) + htmlStream = fromPipeableToWebReadable(errorStream) + } const htmlResponse = new Response( htmlStream.pipeThrough(injectRSCPayload(flightStream2)), { + status, headers: { 'content-type': 'text/html;charset=utf-8', }, From c08e768253f8986aa0cc0c28ebfee83288f71087 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 18:04:04 +0100 Subject: [PATCH 046/100] using react-dom/server.edge fixes hanging on error --- spiceflow/src/react/components.tsx | 6 +- spiceflow/src/react/entry.client.tsx | 12 ++-- spiceflow/src/react/entry.ssr.tsx | 15 ++-- spiceflow/src/{spiceflow.ts => spiceflow.tsx} | 69 ++++++++++--------- 4 files changed, 53 insertions(+), 49 deletions(-) rename spiceflow/src/{spiceflow.ts => spiceflow.tsx} (97%) diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 2b06c04..6cfc858 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -76,7 +76,11 @@ function getErrorContext(error: Error): ReactServerErrorContext | undefined { return (error as any).serverError } -export class ErrorBoundary extends React.Component { +export function ErrorBoundary(props: Props) { + return +} + +class ErrorBoundary_ extends React.Component { constructor(props: Props) { super(props) this.state = { error: null } diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 0527c67..310eabe 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -64,13 +64,11 @@ async function main() { }, []) return ( - Loading root...}> - - - {payload.root?.layouts?.[0]?.element ?? payload.root.page} - - - + + + {payload.root?.layouts?.[0]?.element ?? payload.root.page} + + ) } diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index cf0959e..d6a9bb9 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -1,5 +1,5 @@ import type { IncomingMessage, ServerResponse } from 'node:http' -import ReactDomServer from 'react-dom/server' +import ReactDOMServer from 'react-dom/server.edge' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' import type { ModuleRunner } from 'vite/module-runner' @@ -62,17 +62,15 @@ export default async function handler( let status = 200 try { - const ssrStream = await ReactDomServer.renderToPipeableStream(el, { + htmlStream = await ReactDOMServer.renderToReadableStream(el, { bootstrapModules: ssrAssets.bootstrapModules, - // @ts-ignore formState: payload.formState, onError(error) { - console.error('[react-dom:renderToPipeableStream]', error) - status = 500 + // This also throws outside, no need to do anything here + // console.error('[react-dom:renderToPipeableStream]', error) + // status = 500 }, }) - - htmlStream = fromPipeableToWebReadable(ssrStream) } catch (e) { console.log(`error during ssr render catch`, e) // On error, render minimal HTML shell @@ -89,10 +87,9 @@ export default async function handler( ) - const errorStream = ReactDomServer.renderToPipeableStream(errorRoot, { + htmlStream = await ReactDOMServer.renderToReadableStream(errorRoot, { bootstrapModules: ssrAssets.bootstrapModules, }) - htmlStream = fromPipeableToWebReadable(errorStream) } const htmlResponse = new Response( diff --git a/spiceflow/src/spiceflow.ts b/spiceflow/src/spiceflow.tsx similarity index 97% rename from spiceflow/src/spiceflow.ts rename to spiceflow/src/spiceflow.tsx index 32ab0ed..cadbdc9 100644 --- a/spiceflow/src/spiceflow.ts +++ b/spiceflow/src/spiceflow.tsx @@ -26,7 +26,7 @@ import { RouteSchema, SingletonBase, TypeSchema, - UnwrapRoute + UnwrapRoute, } from './types.js' let globalIndex = 0 @@ -38,15 +38,20 @@ import { MiddlewareContext } from './context.js' import { isProduction, ValidationError } from './error.js' import { isAsyncIterable, isResponse, redirect } from './utils.js' - -import { FlightData, LayoutContent } from './react/components.js' -import { ClientReferenceMetadataManifest, ServerReferenceManifest } from './react/types/index.js' +import { + DefaultGlobalErrorPage, + ErrorBoundary, + FlightData, + LayoutContent, +} from './react/components.js' +import { + ClientReferenceMetadataManifest, + ServerReferenceManifest, +} from './react/types/index.js' import { fromPipeableToWebReadable } from './react/utils/fetch.js' import { TrieRouter } from './trie-router/router.js' import { decodeURIComponent_ } from './trie-router/url.js' -import { - Result -} from './trie-router/utils.js' +import { Result } from './trie-router/utils.js' const ajv = (addFormats.default || addFormats)( new (Ajv.default || Ajv)({ useDefaults: true }), @@ -880,29 +885,35 @@ export class Spiceflow< redirect, } - const [page, ...layouts] = await Promise.all([ - pageRoute?.route?.handler({ - ...baseContext, - state: cloneDeep(pageRoute.app.defaultState), - params: pageRoute.params, - }), - ...layoutRoutes.map(async (layout) => { - const id = layout.route.id - const children = createElement(LayoutContent, { id }) - - return { - element: (await layout.route.handler({ + let Page = pageRoute?.route?.handler as any + let page = ( + + ) + const layouts = layoutRoutes.map((layout) => { + const id = layout.route.id + const children = createElement(LayoutContent, { id }) + + let Layout = layout.route.handler as any + const element = ( + + ) + return { element, id } + }) let root: FlightData = { url: request.url, @@ -942,12 +953,9 @@ export class Spiceflow< } } - - if (root instanceof Response) { return root } - let abortable = ReactServer.renderToPipeableStream( { @@ -966,8 +974,8 @@ export class Spiceflow< return new Response(stream, { headers: { - 'content-type': 'text/x-component;charset=utf-8' - } + 'content-type': 'text/x-component;charset=utf-8', + }, }) } catch (err) { return await getResForError(err) @@ -1676,8 +1684,6 @@ function partition(arr: T[], predicate: (item: T) => boolean): [T[], T[]] { ) } - - const serverReferenceManifest: ServerReferenceManifest = { resolveServerReference(reference: string) { const [id, name] = reference.split('#') @@ -1714,7 +1720,6 @@ const clientReferenceMetadataManifest: ClientReferenceMetadataManifest = { }, } - export interface ServerPayload { root: FlightData formState?: ReactFormState From 3d5d5ed51a89b57829f02a47f573f6206314d464 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 10 Feb 2025 18:07:02 +0100 Subject: [PATCH 047/100] add css in error page --- example-react/package.json | 2 ++ example-react/src/main.tsx | 33 +++++++++++++++++-------------- pnpm-lock.yaml | 8 +++++++- spiceflow/src/react/entry.ssr.tsx | 7 ++++++- 4 files changed, 33 insertions(+), 17 deletions(-) diff --git a/example-react/package.json b/example-react/package.json index b234960..a975b1e 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -16,6 +16,8 @@ "dependencies": { "@playwright/test": "^1.50.1", "@tailwindcss/vite": "^4.0.5", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "react": "19.0.0", "react-dom": "19.0.0", "spiceflow": "workspace:*", diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 172aea9..7b1cdf7 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -1,17 +1,21 @@ import { Spiceflow } from "spiceflow"; +import { Suspense } from "react"; import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; -import './styles.css' +import "./styles.css"; import { ClientComponentThrows } from "./app/client"; - const app = new Spiceflow() .layout("/*", async ({ children, request }) => { - return {children}; + return ( + + Loading...}>{children} + + ); }) .page("/", async ({ request }) => { const url = new URL(request.url); - return ; + return ; }) .get("/hello", () => "Hello, World!") @@ -66,15 +70,15 @@ const app = new Spiceflow() ); }) - .page('/loader-error', async () => { - throw new Error('test error'); - }) - .page('/rsc-error', async () => { - return - }) - .page('/client-error', async () => { - return - }) + .page("/loader-error", async () => { + throw new Error("test error"); + }) + .page("/rsc-error", async () => { + return ; + }) + .page("/client-error", async () => { + return ; + }) .page("/redirect-in-rsc", async () => { return ; }) @@ -94,9 +98,8 @@ async function Redirects() { } function ServerComponentThrows() { - throw new Error('Server component error'); + throw new Error("Server component error"); return
Server component
; } - export default app; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78bd58c..dbfbad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,12 @@ importers: '@tailwindcss/vite': specifier: ^4.0.5 version: 4.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@types/react': + specifier: ^19.0.8 + version: 19.0.8 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.0.3(@types/react@19.0.8) react: specifier: 19.0.0 version: 19.0.0 @@ -7813,7 +7819,7 @@ snapshots: '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.7) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.1 - esbuild: 0.17.6 + esbuild: 0.17.19 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index d6a9bb9..be92425 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -66,7 +66,7 @@ export default async function handler( bootstrapModules: ssrAssets.bootstrapModules, formState: payload.formState, onError(error) { - // This also throws outside, no need to do anything here + // This also throws outside, no need to do anything here // console.error('[react-dom:renderToPipeableStream]', error) // status = 500 }, @@ -80,6 +80,11 @@ export default async function handler( + {cssUrls.map((url) => ( + // precedence to force head rendering + // https://react.dev/reference/react-dom/components/link#special-rendering-behavior + + ))} From ff956c0509121ab0fadd2e05330a6694d25372c2 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 16:10:59 +0100 Subject: [PATCH 048/100] made link component, tested client navigations, back buttons waits which is not great --- example-react/src/app/client.tsx | 2 + example-react/src/app/layout.tsx | 11 +- example-react/src/main.tsx | 12 +- package.json | 2 +- pnpm-lock.yaml | 112 ++++++++++-------- spiceflow/.gitignore | 1 + spiceflow/package.json | 1 + spiceflow/src/react/components.tsx | 24 ++++ spiceflow/src/react/entry.client.tsx | 74 +++--------- spiceflow/src/react/entry.ssr.tsx | 21 +++- spiceflow/src/react/router.tsx | 7 ++ spiceflow/src/react/utils/client-reference.ts | 4 +- spiceflow/src/spiceflow.tsx | 4 +- spiceflow/src/vite.tsx | 1 + spiceflow/tsconfig.json | 1 + spiceflow/vitest.config.js | 10 ++ 16 files changed, 164 insertions(+), 123 deletions(-) create mode 100644 spiceflow/.gitignore create mode 100644 spiceflow/src/react/router.tsx diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index 12d2afa..8203fce 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -70,3 +70,5 @@ export function ClientComponentThrows() { throw new Error('Client component error'); return
Client component
; } + + diff --git a/example-react/src/app/layout.tsx b/example-react/src/app/layout.tsx index f6009c5..b2e8b07 100644 --- a/example-react/src/app/layout.tsx +++ b/example-react/src/app/layout.tsx @@ -1,3 +1,5 @@ +import { Link } from "spiceflow/dist/react/components"; + export function Layout(props: React.PropsWithChildren) { return ( @@ -9,13 +11,16 @@ export function Layout(props: React.PropsWithChildren) { content="width=device-width, height=device-height, initial-scale=1.0" /> - +
{props.children} diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 7b1cdf7..015585e 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -4,6 +4,8 @@ import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; import "./styles.css"; import { ClientComponentThrows } from "./app/client"; +import { ErrorBoundary } from "spiceflow/dist/react/components"; +import { sleep } from "spiceflow/dist/utils"; const app = new Spiceflow() .layout("/*", async ({ children, request }) => { @@ -35,6 +37,14 @@ const app = new Spiceflow() ); }) + .page("/slow", async ({ request, children }) => { + await sleep(1000); + return ( +
+

slow page

+
+ ); + }) .layout("/page/*", async ({ request, children }) => { return (
@@ -97,7 +107,7 @@ async function Redirects() { return
Redirect
; } -function ServerComponentThrows() { +async function ServerComponentThrows() { throw new Error("Server component error"); return
Server component
; } diff --git a/package.json b/package.json index b1bc158..85d9179 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "tsx": "^4.19.2", "typescript": "^5.7.3", "vite": "^6.1.0", - "vitest": "^3.0.4" + "vitest": "^3.0.5" }, "repository": "https://github.com/remorses/", "author": "remorses ", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbfbad3..e3aa8b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,8 +34,8 @@ importers: specifier: ^6.1.0 version: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) vitest: - specifier: ^3.0.4 - version: 3.0.4(@types/debug@4.1.12)(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + specifier: ^3.0.5 + version: 3.0.5(@types/debug@4.1.12)(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) example-react: dependencies: @@ -195,6 +195,9 @@ importers: eventsource-parser: specifier: ^3.0.0 version: 3.0.0 + history: + specifier: ^5.3.0 + version: 5.3.0 lodash.clonedeep: specifier: ^4.5.0 version: 4.5.0 @@ -2203,11 +2206,11 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - '@vitest/expect@3.0.4': - resolution: {integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==} + '@vitest/expect@3.0.5': + resolution: {integrity: sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==} - '@vitest/mocker@3.0.4': - resolution: {integrity: sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==} + '@vitest/mocker@3.0.5': + resolution: {integrity: sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 @@ -2217,20 +2220,20 @@ packages: vite: optional: true - '@vitest/pretty-format@3.0.4': - resolution: {integrity: sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==} + '@vitest/pretty-format@3.0.5': + resolution: {integrity: sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==} - '@vitest/runner@3.0.4': - resolution: {integrity: sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==} + '@vitest/runner@3.0.5': + resolution: {integrity: sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==} - '@vitest/snapshot@3.0.4': - resolution: {integrity: sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==} + '@vitest/snapshot@3.0.5': + resolution: {integrity: sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==} - '@vitest/spy@3.0.4': - resolution: {integrity: sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==} + '@vitest/spy@3.0.5': + resolution: {integrity: sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==} - '@vitest/utils@3.0.4': - resolution: {integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==} + '@vitest/utils@3.0.5': + resolution: {integrity: sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==} '@web3-storage/multipart-parser@1.0.0': resolution: {integrity: sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==} @@ -3459,6 +3462,9 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + history@5.3.0: + resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==} + hosted-git-info@6.1.3: resolution: {integrity: sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3957,8 +3963,8 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.2: - resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -5754,8 +5760,8 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - vite-node@3.0.4: - resolution: {integrity: sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==} + vite-node@3.0.5: + resolution: {integrity: sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -5848,16 +5854,16 @@ packages: yaml: optional: true - vitest@3.0.4: - resolution: {integrity: sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==} + vitest@3.0.5: + resolution: {integrity: sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.4 - '@vitest/ui': 3.0.4 + '@vitest/browser': 3.0.5 + '@vitest/ui': 3.0.5 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -7061,7 +7067,7 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.26.7 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 @@ -7853,44 +7859,44 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/expect@3.0.4': + '@vitest/expect@3.0.5': dependencies: - '@vitest/spy': 3.0.4 - '@vitest/utils': 3.0.4 + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 chai: 5.1.2 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@vitest/mocker@3.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: - '@vitest/spy': 3.0.4 + '@vitest/spy': 3.0.5 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) - '@vitest/pretty-format@3.0.4': + '@vitest/pretty-format@3.0.5': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.0.4': + '@vitest/runner@3.0.5': dependencies: - '@vitest/utils': 3.0.4 + '@vitest/utils': 3.0.5 pathe: 2.0.2 - '@vitest/snapshot@3.0.4': + '@vitest/snapshot@3.0.5': dependencies: - '@vitest/pretty-format': 3.0.4 + '@vitest/pretty-format': 3.0.5 magic-string: 0.30.17 pathe: 2.0.2 - '@vitest/spy@3.0.4': + '@vitest/spy@3.0.5': dependencies: tinyspy: 3.0.2 - '@vitest/utils@3.0.4': + '@vitest/utils@3.0.5': dependencies: - '@vitest/pretty-format': 3.0.4 - loupe: 3.1.2 + '@vitest/pretty-format': 3.0.5 + loupe: 3.1.3 tinyrainbow: 2.0.0 '@web3-storage/multipart-parser@1.0.0': {} @@ -8210,7 +8216,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.2 + loupe: 3.1.3 pathval: 2.0.0 chalk-template@0.4.0: @@ -9463,6 +9469,10 @@ snapshots: dependencies: '@types/hast': 3.0.4 + history@5.3.0: + dependencies: + '@babel/runtime': 7.26.7 + hosted-git-info@6.1.3: dependencies: lru-cache: 7.18.3 @@ -9897,7 +9907,7 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.2: {} + loupe@3.1.3: {} lru-cache@10.4.3: {} @@ -12400,7 +12410,7 @@ snapshots: - supports-color - terser - vite-node@3.0.4(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vite-node@3.0.5(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 @@ -12468,15 +12478,15 @@ snapshots: tsx: 4.19.2 yaml: 2.7.0 - vitest@3.0.4(@types/debug@4.1.12)(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + vitest@3.0.5(@types/debug@4.1.12)(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: - '@vitest/expect': 3.0.4 - '@vitest/mocker': 3.0.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) - '@vitest/pretty-format': 3.0.4 - '@vitest/runner': 3.0.4 - '@vitest/snapshot': 3.0.4 - '@vitest/spy': 3.0.4 - '@vitest/utils': 3.0.4 + '@vitest/expect': 3.0.5 + '@vitest/mocker': 3.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/pretty-format': 3.0.5 + '@vitest/runner': 3.0.5 + '@vitest/snapshot': 3.0.5 + '@vitest/spy': 3.0.5 + '@vitest/utils': 3.0.5 chai: 5.1.2 debug: 4.4.0 expect-type: 1.1.0 @@ -12488,7 +12498,7 @@ snapshots: tinypool: 1.0.2 tinyrainbow: 2.0.0 vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) - vite-node: 3.0.4(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + vite-node: 3.0.5(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 diff --git a/spiceflow/.gitignore b/spiceflow/.gitignore new file mode 100644 index 0000000..b2b5d0d --- /dev/null +++ b/spiceflow/.gitignore @@ -0,0 +1 @@ +debug \ No newline at end of file diff --git a/spiceflow/package.json b/spiceflow/package.json index 6a2c030..f1813d2 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -63,6 +63,7 @@ "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "eventsource-parser": "^3.0.0", + "history": "^5.3.0", "lodash.clonedeep": "^4.5.0", "object-treeify": "^5.0.1", "openapi-types": "^12.1.3", diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 6cfc858..808cdb7 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -2,6 +2,7 @@ import React, { Suspense } from 'react' import { ReactFormState } from 'react-dom/client' +import { router } from './router.js' export const FlightDataContext = React.createContext(undefined!) // Get $$id property that was set by registerClientReference @@ -158,3 +159,26 @@ export function DefaultGlobalErrorPage(props: ErrorPageProps) { ) } + +export function Link(props: React.ComponentPropsWithRef<'a'>) { + return ( + { + if ( + e.metaKey || + e.ctrlKey || + e.shiftKey || + e.altKey || + (props.target && props.target === '_blank') + ) { + props.onClick?.(e) + return + } + e.preventDefault() + props.onClick?.(e) + router.push(e.currentTarget.href) + }} + /> + ) +} diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 310eabe..a83fa49 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,4 +1,5 @@ import React, { Suspense } from 'react' +import { router } from './router.js' import ReactDomClient from 'react-dom/client' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' @@ -24,23 +25,12 @@ async function main() { clientReferenceManifest, { callServer }, ) + // console.log({ 'action payload': payload }) setPayload(payload) return payload.returnValue } Object.assign(globalThis, { __callServer: callServer }) - async function onNavigation() { - const url = new URL(window.location.href) - url.searchParams.set('__rsc', '') - const payload = await ReactClient.createFromFetch( - fetch(url), - clientReferenceManifest, - - { callServer }, - ) - setPayload(payload) - } - const initialPayload = await ReactClient.createFromReadableStream( rscStream, @@ -60,7 +50,18 @@ async function main() { }, [startTransition, setPayload_]) React.useEffect(() => { - return listenNavigation(onNavigation) + return router.listen(async function onNavigation() { + console.log('onNavigation') + const url = new URL(window.location.href) + url.searchParams.set('__rsc', '') + const payload = await ReactClient.createFromFetch( + fetch(url), + clientReferenceManifest, + + { callServer }, + ) + setPayload(payload) + }) }, []) return ( @@ -84,51 +85,4 @@ async function main() { } } -function listenNavigation(onNavigation: () => void) { - window.addEventListener('popstate', onNavigation) - - const oldPushState = window.history.pushState - window.history.pushState = function (...args) { - const res = oldPushState.apply(this, args) - onNavigation() - return res - } - - const oldReplaceState = window.history.replaceState - window.history.replaceState = function (...args) { - const res = oldReplaceState.apply(this, args) - onNavigation() - return res - } - - function onClick(e: MouseEvent) { - let link = (e.target as Element).closest('a') - if ( - link && - link instanceof HTMLAnchorElement && - link.href && - (!link.target || link.target === '_self') && - link.origin === location.origin && - !link.hasAttribute('download') && - e.button === 0 && // left clicks only - !e.metaKey && // open in new tab (mac) - !e.ctrlKey && // open in new tab (windows) - !e.altKey && // download - !e.shiftKey && - !e.defaultPrevented - ) { - e.preventDefault() - history.pushState(null, '', link.href) - } - } - document.addEventListener('click', onClick) - - return () => { - document.removeEventListener('click', onClick) - window.removeEventListener('popstate', onNavigation) - window.history.pushState = oldPushState - window.history.replaceState = oldReplaceState - } -} - main() diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index be92425..a5a2878 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -20,6 +20,7 @@ import { clientReferenceManifest } from './utils/client-reference.js' import cssUrls from 'virtual:app-styles' import { ServerPayload } from '../spiceflow.js' import { Suspense } from 'react' +import { sleep } from 'spiceflow/dist/utils' export default async function handler( req: IncomingMessage, @@ -65,17 +66,27 @@ export default async function handler( htmlStream = await ReactDOMServer.renderToReadableStream(el, { bootstrapModules: ssrAssets.bootstrapModules, formState: payload.formState, - onError(error) { + onError(e, ) { // This also throws outside, no need to do anything here - // console.error('[react-dom:renderToPipeableStream]', error) - // status = 500 + console.error('[react-dom:renderToPipeableStream]', e) + if (e instanceof Response) { + console.log('sending response') + sendResponse(e, res) + return + } }, }) } catch (e) { console.log(`error during ssr render catch`, e) // On error, render minimal HTML shell // Client will do full CSR render and show error boundary - status = 500 + + if (e instanceof Response) { + sendResponse(e, res) + return + } + // https://bsky.app/profile/ebey.bsky.social/post/3lev4lqr2ak2j + const errorRoot = ( @@ -106,6 +117,8 @@ export default async function handler( }, }, ) + + console.log(`sending response`) sendResponse(htmlResponse, res) } diff --git a/spiceflow/src/react/router.tsx b/spiceflow/src/react/router.tsx new file mode 100644 index 0000000..8bb4234 --- /dev/null +++ b/spiceflow/src/react/router.tsx @@ -0,0 +1,7 @@ + +import { createBrowserHistory, createMemoryHistory } from 'history' + +export const router = + typeof window === 'undefined' + ? createMemoryHistory() + : createBrowserHistory({}) diff --git a/spiceflow/src/react/utils/client-reference.ts b/spiceflow/src/react/utils/client-reference.ts index 36d7fb9..b38d262 100644 --- a/spiceflow/src/react/utils/client-reference.ts +++ b/spiceflow/src/react/utils/client-reference.ts @@ -8,8 +8,8 @@ export const clientReferenceManifest: ClientReferenceManifest = { async preload() { let mod: Record if (import.meta.env.DEV) { - // console.log('importing client reference', id) - console.log('importing client reference', id) + + // console.log('importing client reference', id) mod = typeof __raw_import !== 'undefined' ? // on browser development need to use __raw_import to not add ?import at the end, otherwise the browser duplicates the module instance, context stops working diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index cadbdc9..f15faf4 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -1,5 +1,5 @@ import type { ReactFormState } from 'react-dom/client' -import ReactServer from 'spiceflow/dist/react/server-dom-optimized' + import addFormats from 'ajv-formats' import lodashCloneDeep from 'lodash.clonedeep' @@ -863,6 +863,7 @@ export class Spiceflow< (x) => !x.route.kind, ) if (reactRoutes.length) { + const ReactServer = await import('spiceflow/dist/react/server-dom-optimized').then(m => m.default) const [pageRoutes, layoutRoutes] = partition( reactRoutes, (x) => x.route.kind === 'page', @@ -936,6 +937,7 @@ export class Spiceflow< serverReferenceManifest.resolveServerReference(actionId) await reference.preload() const action = await reference.get() + // TODO handle action errors, redirects, etc returnValue = await (action as any).apply(null, args) } else { // progressive enhancement diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index c67e70b..394344a 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -138,6 +138,7 @@ export function spiceflowPlugin({ entry }): PluginOption { optimizeDeps: { include: [ 'react-dom/client', + 'react-server-dom-vite/client', 'spiceflow/dist/react/server-dom-client-optimized', ], }, diff --git a/spiceflow/tsconfig.json b/spiceflow/tsconfig.json index 9c18dab..bea90bb 100644 --- a/spiceflow/tsconfig.json +++ b/spiceflow/tsconfig.json @@ -10,6 +10,7 @@ "sourceMap": true, "jsx": "react-jsx", "resolveJsonModule": true, + "useUnknownInCatchVariables": false, "outDir": "dist" }, "include": ["src"] diff --git a/spiceflow/vitest.config.js b/spiceflow/vitest.config.js index 391bf1a..820c576 100644 --- a/spiceflow/vitest.config.js +++ b/spiceflow/vitest.config.js @@ -1,5 +1,6 @@ // vite.config.ts import { defineConfig } from 'vite' +import { spiceflowPlugin } from './dist/vite' const execArgv = process.env.PROFILE ? ['--cpu-prof', '--cpu-prof-dir=./profiling'] @@ -9,6 +10,15 @@ export default defineConfig({ esbuild: { jsx: 'transform', }, + // plugins: [ + // spiceflowPlugin({ + // // options + // }), + // ], + resolve: { + conditions: ['react-server'], + }, + test: { exclude: ['**/dist/**', '**/esm/**', '**/node_modules/**', '**/e2e/**'], pool: 'threads', From 4f9e4c0e806ef84bf0d3e8bb55500a3d0786c9d2 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 16:19:05 +0100 Subject: [PATCH 049/100] fix hmr from server, tested suspense in rsc --- example-react/src/app/index.tsx | 1 + example-react/src/app/layout.tsx | 3 +++ example-react/src/main.tsx | 16 ++++++++++++++++ spiceflow/src/react/entry.client.tsx | 2 +- spiceflow/src/spiceflow.tsx | 23 +++++++++++------------ 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/example-react/src/app/index.tsx b/example-react/src/app/index.tsx index 6f8452c..f7bb019 100644 --- a/example-react/src/app/index.tsx +++ b/example-react/src/app/index.tsx @@ -14,6 +14,7 @@ export async function IndexPage() { style={{ padding: "0.5rem" }} >
Server counter: {getCounter()}
+
Unicode test: 🌟 你好 こんにちは ⚡️ 안녕하세요
+ ); + }) + .page("/slow-suspense", async ({ request, children }) => { await sleep(1000); return (
diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index a83fa49..a877f33 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -80,7 +80,7 @@ async function main() { if (import.meta.hot) { import.meta.hot.on('react-server:update', (e) => { console.log('[react-server:update]', e.file) - window.history.replaceState({}, '', window.location.href) + router.replace(router.location) }) } } diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index f15faf4..20f978e 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -1,6 +1,5 @@ import type { ReactFormState } from 'react-dom/client' - import addFormats from 'ajv-formats' import lodashCloneDeep from 'lodash.clonedeep' @@ -246,15 +245,11 @@ export class Spiceflow< } // Get all matched routes - const routes = matchedRoutes[0] - .map(([route, params], index) => ({ - app, - route, - params: this.getAllDecodedParams(matchedRoutes, originalPath, index), - })) - .sort((a, b) => { - return routeSorter(a.route, b.route) - }) + const routes = matchedRoutes[0].map(([route, params], index) => ({ + app, + route, + params: this.getAllDecodedParams(matchedRoutes, originalPath, index), + })) if (routes.length) { return routes @@ -863,7 +858,9 @@ export class Spiceflow< (x) => !x.route.kind, ) if (reactRoutes.length) { - const ReactServer = await import('spiceflow/dist/react/server-dom-optimized').then(m => m.default) + const ReactServer = await import( + 'spiceflow/dist/react/server-dom-optimized' + ).then((m) => m.default) const [pageRoutes, layoutRoutes] = partition( reactRoutes, (x) => x.route.kind === 'page', @@ -983,7 +980,9 @@ export class Spiceflow< return await getResForError(err) } } - const route = nonReactRoutes[0] + const route = nonReactRoutes.sort((a, b) => { + return routeSorter(a.route, b.route) + })[0] // TODO get all apps in scope? layouts can match between apps when using .use? const appsInScope = this.getAppsInScope(routes[0].app) From 692e63111cd28f803a0c79c4ada36aa599c34a28 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 17:58:02 +0100 Subject: [PATCH 050/100] added how-is-this-not-illegal, even simpler --- example-react/src/app/layout.tsx | 1 - example-react/src/main.tsx | 14 +- how-is-this-not-illegal/.gitignore | 36 + how-is-this-not-illegal/app/globals.css | 35 + how-is-this-not-illegal/app/main.tsx | 137 ++ .../app/opengraph-image.js | 92 ++ how-is-this-not-illegal/package.json | 27 + how-is-this-not-illegal/public/favicon.ico | Bin 0 -> 25931 bytes how-is-this-not-illegal/sql/destroy.sql | 2 + how-is-this-not-illegal/sql/init.sql | 159 +++ how-is-this-not-illegal/tsconfig.json | 29 + how-is-this-not-illegal/vite.config.ts | 14 + pnpm-lock.yaml | 1153 ++++++++++++----- spiceflow/package.json | 1 - spiceflow/src/vite.tsx | 1 - 15 files changed, 1364 insertions(+), 337 deletions(-) create mode 100644 how-is-this-not-illegal/.gitignore create mode 100644 how-is-this-not-illegal/app/globals.css create mode 100644 how-is-this-not-illegal/app/main.tsx create mode 100644 how-is-this-not-illegal/app/opengraph-image.js create mode 100644 how-is-this-not-illegal/package.json create mode 100644 how-is-this-not-illegal/public/favicon.ico create mode 100644 how-is-this-not-illegal/sql/destroy.sql create mode 100644 how-is-this-not-illegal/sql/init.sql create mode 100644 how-is-this-not-illegal/tsconfig.json create mode 100644 how-is-this-not-illegal/vite.config.ts diff --git a/example-react/src/app/layout.tsx b/example-react/src/app/layout.tsx index 79e1cfd..e6d9744 100644 --- a/example-react/src/app/layout.tsx +++ b/example-react/src/app/layout.tsx @@ -5,7 +5,6 @@ export function Layout(props: React.PropsWithChildren) { - react-server { return ( - Loading...
}>{children} + title from layout + {children} ); }) .page("/", async ({ request }) => { const url = new URL(request.url); - return ; + return ( + <> + title from page + + + ); }) .get("/hello", () => "Hello, World!") @@ -49,7 +55,9 @@ const app = new Spiceflow() return (

/slow-suspense layout

- Loading slow page layout...
}>{children} + Loading slow page layout...
}> + {children} + ); }) diff --git a/how-is-this-not-illegal/.gitignore b/how-is-this-not-illegal/.gitignore new file mode 100644 index 0000000..3161618 --- /dev/null +++ b/how-is-this-not-illegal/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts +.env \ No newline at end of file diff --git a/how-is-this-not-illegal/app/globals.css b/how-is-this-not-illegal/app/globals.css new file mode 100644 index 0000000..c9d06d1 --- /dev/null +++ b/how-is-this-not-illegal/app/globals.css @@ -0,0 +1,35 @@ +@import 'tailwindcss'; + +:root { + --foreground-rgb: 0, 0, 0; + --background-start-rgb: 214, 219, 220; + --background-end-rgb: 255, 255, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --foreground-rgb: 255, 255, 255; + --background-start-rgb: 34, 34, 34; + --background-end-rgb: 0, 0, 0; + } +} + +body { + color: rgb(var(--foreground-rgb)); + background: linear-gradient( + to bottom, + transparent, + rgb(var(--background-end-rgb)) + ) + rgb(var(--background-start-rgb)); +} + + +@layer utilities { + .bg-gradient-radial { + background-image: radial-gradient(var(--tw-gradient-stops)); + } + .bg-gradient-conic { + background-image: conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops)); + } +} diff --git a/how-is-this-not-illegal/app/main.tsx b/how-is-this-not-illegal/app/main.tsx new file mode 100644 index 0000000..e3f4c98 --- /dev/null +++ b/how-is-this-not-illegal/app/main.tsx @@ -0,0 +1,137 @@ +import './globals.css' +import { Spiceflow } from 'spiceflow' +import { sql } from '@vercel/postgres' +import { Suspense } from 'react' + +const app = new Spiceflow() + .layout('/*', async ({ children }) => { + return ( + + }>{children} + + ) + }) + .page('/', async function Home() { + const { rows } = await sql`SELECT * FROM pokemon ORDER BY RANDOM() LIMIT 12` + + return ( + + {rows.map((p) => ( + + ))} + + ) + }) + +function Loading() { + return ( +
    + {[...Array(12)].map((_, i) => ( +
  • +
    + + Loading + +
  • + ))} +
+ ) +} + +function PokemonList({ children }) { + return ( +
    + {children} +
+ ) +} + +function Pokemon({ id, name }) { + return ( +
  • + {name} + {name} +
  • + ) +} + +function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + How is this not illegal? + + + + + + + + + + +
    +

    How is this not illegal?

    +

    + This page renders{' '} + + SELECT * FROM pokemon ORDER BY RANDOM() LIMIT 12 + {' '} + from the edge, for every request. +

    +

    + What's best, the data fetching is defined directly within the + component tree thanks to React Server Components.{' '} + + Legally + + . ( + + Source + + ) +

    + {children} +
    + +
    + Images courtesy of{' '} + + PokeAPI + {' '} + – Pokemon is © 1996-2023 Nintendo, Creatures, Inc., GAME FREAK +
    + + + ) +} + +export default app diff --git a/how-is-this-not-illegal/app/opengraph-image.js b/how-is-this-not-illegal/app/opengraph-image.js new file mode 100644 index 0000000..53f90ea --- /dev/null +++ b/how-is-this-not-illegal/app/opengraph-image.js @@ -0,0 +1,92 @@ +import { sql } from "@vercel/postgres"; +import { ImageResponse } from "next/server"; +import Image from "next/image"; + +export const runtime = "edge"; +export const revalidate = 60; + +export default async function OGImage() { + const { rows } = await sql`SELECT * FROM pokemon ORDER BY RANDOM() LIMIT 12`; + + const inter500 = fetch( + new URL( + `../node_modules/@fontsource/inter/files/inter-latin-500-normal.woff`, + import.meta.url + ) + ).then((res) => res.arrayBuffer()); + + const robotoMono400 = fetch( + new URL( + `../node_modules/@fontsource/roboto-mono/files/roboto-mono-latin-400-normal.woff`, + import.meta.url + ) + ).then((res) => res.arrayBuffer()); + + return new ImageResponse( + ( +
    +
    +

    How is this not illegal?

    + +

    + + await sql`SELECT * FROM pokemon ORDER BY RANDOM() LIMIT 12` + {" "} + right in your{" "} + + <Component /> + +

    + +
      + {rows.map(({ id, name }) => ( +
    • + {/* eslint-disable-next-line @next/next/no-img-element */} + {name} + {name} +
    • + ))} +
    +
    +
    + ), + { + width: 1200, + height: 630, + fonts: [ + { + name: "Inter 500", + data: await inter500, + }, + { + name: "Roboto Mono 400", + data: await robotoMono400, + }, + ], + } + ); +} + +function font(fontFamily) { + return { fontFamily }; +} diff --git a/how-is-this-not-illegal/package.json b/how-is-this-not-illegal/package.json new file mode 100644 index 0000000..aaa33d8 --- /dev/null +++ b/how-is-this-not-illegal/package.json @@ -0,0 +1,27 @@ +{ + "name": "how-is-this-not-illegal", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "dotenv -- vite dev", + "seed": "dotenv -- bash -c 'psql \"$POSTGRES_URL\" -f sql/init.sql'", + "build": "dotenv -- vite build --app" + }, + "dependencies": { + "@fontsource/inter": "^4.5.15", + "@fontsource/roboto-mono": "^4.5.10", + "@tailwindcss/vite": "^4.0.6", + "@types/node": "18.16.3", + "@types/react": "19.0.8", + "@types/react-dom": "19.0.3", + "@vercel/postgres": "0.4.1", + "dotenv-cli": "^8.0.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "spiceflow": "workspace:*", + "tailwindcss": "4.0.6", + "typescript": "5.7.3", + "vite": "^6.1.0" + } +} diff --git a/how-is-this-not-illegal/public/favicon.ico b/how-is-this-not-illegal/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..718d6fea4835ec2d246af9800eddb7ffb276240c GIT binary patch literal 25931 zcmeHv30#a{`}aL_*G&7qml|y<+KVaDM2m#dVr!KsA!#An?kSQM(q<_dDNCpjEux83 zLb9Z^XxbDl(w>%i@8hT6>)&Gu{h#Oeyszu?xtw#Zb1mO{pgX9699l+Qppw7jXaYf~-84xW z)w4x8?=youko|}Vr~(D$UXIbiXABHh`p1?nn8Po~fxRJv}|0e(BPs|G`(TT%kKVJAdg5*Z|x0leQq0 zkdUBvb#>9F()jo|T~kx@OM8$9wzs~t2l;K=woNssA3l6|sx2r3+kdfVW@e^8e*E}v zA1y5{bRi+3Z`uD3{F7LgFJDdvm;nJilkzDku>BwXH(8ItVCXk*-lSJnR?-2UN%hJ){&rlvg`CDTj z)Bzo!3v7Ou#83zEDEFcKt(f1E0~=rqeEbTnMvWR#{+9pg%7G8y>u1OVRUSoox-ovF z2Ydma(;=YuBY(eI|04{hXzZD6_f(v~H;C~y5=DhAC{MMS>2fm~1H_t2$56pc$NH8( z5bH|<)71dV-_oCHIrzrT`2s-5w_+2CM0$95I6X8p^r!gHp+j_gd;9O<1~CEQQGS8) zS9Qh3#p&JM-G8rHekNmKVewU;pJRcTAog68KYo^dRo}(M>36U4Us zfgYWSiHZL3;lpWT=zNAW>Dh#mB!_@Lg%$ms8N-;aPqMn+C2HqZgz&9~Eu z4|Kp<`$q)Uw1R?y(~S>ePdonHxpV1#eSP1B;Ogo+-Pk}6#0GsZZ5!||ev2MGdh}_m z{DeR7?0-1^zVs&`AV6Vt;r3`I`OI_wgs*w=eO%_#7Kepl{B@xiyCANc(l zzIyd4y|c6PXWq9-|KM8(zIk8LPk(>a)zyFWjhT!$HJ$qX1vo@d25W<fvZQ2zUz5WRc(UnFMKHwe1| zWmlB1qdbiA(C0jmnV<}GfbKtmcu^2*P^O?MBLZKt|As~ge8&AAO~2K@zbXelK|4T<{|y4`raF{=72kC2Kn(L4YyenWgrPiv z@^mr$t{#X5VuIMeL!7Ab6_kG$&#&5p*Z{+?5U|TZ`B!7llpVmp@skYz&n^8QfPJzL z0G6K_OJM9x+Wu2gfN45phANGt{7=C>i34CV{Xqlx(fWpeAoj^N0Biu`w+MVcCUyU* zDZuzO0>4Z6fbu^T_arWW5n!E45vX8N=bxTVeFoep_G#VmNlQzAI_KTIc{6>c+04vr zx@W}zE5JNSU>!THJ{J=cqjz+4{L4A{Ob9$ZJ*S1?Ggg3klFp!+Y1@K+pK1DqI|_gq z5ZDXVpge8-cs!o|;K73#YXZ3AShj50wBvuq3NTOZ`M&qtjj#GOFfgExjg8Gn8>Vq5 z`85n+9|!iLCZF5$HJ$Iu($dm?8~-ofu}tEc+-pyke=3!im#6pk_Wo8IA|fJwD&~~F zc16osQ)EBo58U7XDuMexaPRjU@h8tXe%S{fA0NH3vGJFhuyyO!Uyl2^&EOpX{9As0 zWj+P>{@}jxH)8|r;2HdupP!vie{sJ28b&bo!8`D^x}TE$%zXNb^X1p@0PJ86`dZyj z%ce7*{^oo+6%&~I!8hQy-vQ7E)0t0ybH4l%KltWOo~8cO`T=157JqL(oq_rC%ea&4 z2NcTJe-HgFjNg-gZ$6!Y`SMHrlj}Etf7?r!zQTPPSv}{so2e>Fjs1{gzk~LGeesX%r(Lh6rbhSo_n)@@G-FTQy93;l#E)hgP@d_SGvyCp0~o(Y;Ee8{ zdVUDbHm5`2taPUOY^MAGOw*>=s7=Gst=D+p+2yON!0%Hk` zz5mAhyT4lS*T3LS^WSxUy86q&GnoHxzQ6vm8)VS}_zuqG?+3td68_x;etQAdu@sc6 zQJ&5|4(I?~3d-QOAODHpZ=hlSg(lBZ!JZWCtHHSj`0Wh93-Uk)_S%zsJ~aD>{`A0~ z9{AG(e|q3g5B%wYKRxiL2Y$8(4w6bzchKuloQW#e&S3n+P- z8!ds-%f;TJ1>)v)##>gd{PdS2Oc3VaR`fr=`O8QIO(6(N!A?pr5C#6fc~Ge@N%Vvu zaoAX2&(a6eWy_q&UwOhU)|P3J0Qc%OdhzW=F4D|pt0E4osw;%<%Dn58hAWD^XnZD= z>9~H(3bmLtxpF?a7su6J7M*x1By7YSUbxGi)Ot0P77`}P3{)&5Un{KD?`-e?r21!4vTTnN(4Y6Lin?UkSM z`MXCTC1@4A4~mvz%Rh2&EwY))LeoT=*`tMoqcEXI>TZU9WTP#l?uFv+@Dn~b(>xh2 z;>B?;Tz2SR&KVb>vGiBSB`@U7VIWFSo=LDSb9F{GF^DbmWAfpms8Sx9OX4CnBJca3 zlj9(x!dIjN?OG1X4l*imJNvRCk}F%!?SOfiOq5y^mZW)jFL@a|r-@d#f7 z2gmU8L3IZq0ynIws=}~m^#@&C%J6QFo~Mo4V`>v7MI-_!EBMMtb%_M&kvAaN)@ZVw z+`toz&WG#HkWDjnZE!6nk{e-oFdL^$YnbOCN}JC&{$#$O27@|Tn-skXr)2ml2~O!5 zX+gYoxhoc7qoU?C^3~&!U?kRFtnSEecWuH0B0OvLodgUAi}8p1 zrO6RSXHH}DMc$&|?D004DiOVMHV8kXCP@7NKB zgaZq^^O<7PoKEp72kby@W0Z!Y*Ay{&vfg#C&gG@YVR9g?FEocMUi1gSN$+V+ayF45{a zuDZDTN}mS|;BO%gEf}pjBfN2-gIrU#G5~cucA;dokXW89%>AyXJJI z9X4UlIWA|ZYHgbI z5?oFk@A=Ik7lrEQPDH!H+b`7_Y~aDb_qa=B2^Y&Ow41cU=4WDd40dp5(QS-WMN-=Y z9g;6_-JdNU;|6cPwf$ak*aJIcwL@1n$#l~zi{c{EW?T;DaW*E8DYq?Umtz{nJ&w-M zEMyTDrC&9K$d|kZe2#ws6)L=7K+{ zQw{XnV6UC$6-rW0emqm8wJoeZK)wJIcV?dST}Z;G0Arq{dVDu0&4kd%N!3F1*;*pW zR&qUiFzK=@44#QGw7k1`3t_d8&*kBV->O##t|tonFc2YWrL7_eqg+=+k;!F-`^b8> z#KWCE8%u4k@EprxqiV$VmmtiWxDLgnGu$Vs<8rppV5EajBXL4nyyZM$SWVm!wnCj-B!Wjqj5-5dNXukI2$$|Bu3Lrw}z65Lc=1G z^-#WuQOj$hwNGG?*CM_TO8Bg-1+qc>J7k5c51U8g?ZU5n?HYor;~JIjoWH-G>AoUP ztrWWLbRNqIjW#RT*WqZgPJXU7C)VaW5}MiijYbABmzoru6EmQ*N8cVK7a3|aOB#O& zBl8JY2WKfmj;h#Q!pN%9o@VNLv{OUL?rixHwOZuvX7{IJ{(EdPpuVFoQqIOa7giLVkBOKL@^smUA!tZ1CKRK}#SSM)iQHk)*R~?M!qkCruaS!#oIL1c z?J;U~&FfH#*98^G?i}pA{ z9Jg36t4=%6mhY(quYq*vSxptes9qy|7xSlH?G=S@>u>Ebe;|LVhs~@+06N<4CViBk zUiY$thvX;>Tby6z9Y1edAMQaiH zm^r3v#$Q#2T=X>bsY#D%s!bhs^M9PMAcHbCc0FMHV{u-dwlL;a1eJ63v5U*?Q_8JO zT#50!RD619#j_Uf))0ooADz~*9&lN!bBDRUgE>Vud-i5ck%vT=r^yD*^?Mp@Q^v+V zG#-?gKlr}Eeqifb{|So?HM&g91P8|av8hQoCmQXkd?7wIJwb z_^v8bbg`SAn{I*4bH$u(RZ6*xUhuA~hc=8czK8SHEKTzSxgbwi~9(OqJB&gwb^l4+m`k*Q;_?>Y-APi1{k zAHQ)P)G)f|AyjSgcCFps)Fh6Bca*Xznq36!pV6Az&m{O8$wGFD? zY&O*3*J0;_EqM#jh6^gMQKpXV?#1?>$ml1xvh8nSN>-?H=V;nJIwB07YX$e6vLxH( zqYwQ>qxwR(i4f)DLd)-$P>T-no_c!LsN@)8`e;W@)-Hj0>nJ-}Kla4-ZdPJzI&Mce zv)V_j;(3ERN3_@I$N<^|4Lf`B;8n+bX@bHbcZTopEmDI*Jfl)-pFDvo6svPRoo@(x z);_{lY<;);XzT`dBFpRmGrr}z5u1=pC^S-{ce6iXQlLGcItwJ^mZx{m$&DA_oEZ)B{_bYPq-HA zcH8WGoBG(aBU_j)vEy+_71T34@4dmSg!|M8Vf92Zj6WH7Q7t#OHQqWgFE3ARt+%!T z?oLovLVlnf?2c7pTc)~cc^($_8nyKwsN`RA-23ed3sdj(ys%pjjM+9JrctL;dy8a( z@en&CQmnV(()bu|Y%G1-4a(6x{aLytn$T-;(&{QIJB9vMox11U-1HpD@d(QkaJdEb zG{)+6Dos_L+O3NpWo^=gR?evp|CqEG?L&Ut#D*KLaRFOgOEK(Kq1@!EGcTfo+%A&I z=dLbB+d$u{sh?u)xP{PF8L%;YPPW53+@{>5W=Jt#wQpN;0_HYdw1{ksf_XhO4#2F= zyPx6Lx2<92L-;L5PD`zn6zwIH`Jk($?Qw({erA$^bC;q33hv!d!>%wRhj# zal^hk+WGNg;rJtb-EB(?czvOM=H7dl=vblBwAv>}%1@{}mnpUznfq1cE^sgsL0*4I zJ##!*B?=vI_OEVis5o+_IwMIRrpQyT_Sq~ZU%oY7c5JMIADzpD!Upz9h@iWg_>>~j zOLS;wp^i$-E?4<_cp?RiS%Rd?i;f*mOz=~(&3lo<=@(nR!_Rqiprh@weZlL!t#NCc zO!QTcInq|%#>OVgobj{~ixEUec`E25zJ~*DofsQdzIa@5^nOXj2T;8O`l--(QyU^$t?TGY^7#&FQ+2SS3B#qK*k3`ye?8jUYSajE5iBbJls75CCc(m3dk{t?- zopcER9{Z?TC)mk~gpi^kbbu>b-+a{m#8-y2^p$ka4n60w;Sc2}HMf<8JUvhCL0B&Btk)T`ctE$*qNW8L$`7!r^9T+>=<=2qaq-;ll2{`{Rg zc5a0ZUI$oG&j-qVOuKa=*v4aY#IsoM+1|c4Z)<}lEDvy;5huB@1RJPquU2U*U-;gu z=En2m+qjBzR#DEJDO`WU)hdd{Vj%^0V*KoyZ|5lzV87&g_j~NCjwv0uQVqXOb*QrQ zy|Qn`hxx(58c70$E;L(X0uZZ72M1!6oeg)(cdKO ze0gDaTz+ohR-#d)NbAH4x{I(21yjwvBQfmpLu$)|m{XolbgF!pmsqJ#D}(ylp6uC> z{bqtcI#hT#HW=wl7>p!38sKsJ`r8}lt-q%Keqy%u(xk=yiIJiUw6|5IvkS+#?JTBl z8H5(Q?l#wzazujH!8o>1xtn8#_w+397*_cy8!pQGP%K(Ga3pAjsaTbbXJlQF_+m+-UpUUent@xM zg%jqLUExj~o^vQ3Gl*>wh=_gOr2*|U64_iXb+-111aH}$TjeajM+I20xw(((>fej-@CIz4S1pi$(#}P7`4({6QS2CaQS4NPENDp>sAqD z$bH4KGzXGffkJ7R>V>)>tC)uax{UsN*dbeNC*v}#8Y#OWYwL4t$ePR?VTyIs!wea+ z5Urmc)X|^`MG~*dS6pGSbU+gPJoq*^a=_>$n4|P^w$sMBBy@f*Z^Jg6?n5?oId6f{ z$LW4M|4m502z0t7g<#Bx%X;9<=)smFolV&(V^(7Cv2-sxbxopQ!)*#ZRhTBpx1)Fc zNm1T%bONzv6@#|dz(w02AH8OXe>kQ#1FMCzO}2J_mST)+ExmBr9cva-@?;wnmWMOk z{3_~EX_xadgJGv&H@zK_8{(x84`}+c?oSBX*Ge3VdfTt&F}yCpFP?CpW+BE^cWY0^ zb&uBN!Ja3UzYHK-CTyA5=L zEMW{l3Usky#ly=7px648W31UNV@K)&Ub&zP1c7%)`{);I4b0Q<)B}3;NMG2JH=X$U zfIW4)4n9ZM`-yRj67I)YSLDK)qfUJ_ij}a#aZN~9EXrh8eZY2&=uY%2N0UFF7<~%M zsB8=erOWZ>Ct_#^tHZ|*q`H;A)5;ycw*IcmVxi8_0Xk}aJA^ath+E;xg!x+As(M#0=)3!NJR6H&9+zd#iP(m0PIW8$ z1Y^VX`>jm`W!=WpF*{ioM?C9`yOR>@0q=u7o>BP-eSHqCgMDj!2anwH?s%i2p+Q7D zzszIf5XJpE)IG4;d_(La-xenmF(tgAxK`Y4sQ}BSJEPs6N_U2vI{8=0C_F?@7<(G; zo$~G=8p+076G;`}>{MQ>t>7cm=zGtfbdDXm6||jUU|?X?CaE?(<6bKDYKeHlz}DA8 zXT={X=yp_R;HfJ9h%?eWvQ!dRgz&Su*JfNt!Wu>|XfU&68iRikRrHRW|ZxzRR^`eIGt zIeiDgVS>IeExKVRWW8-=A=yA`}`)ZkWBrZD`hpWIxBGkh&f#ijr449~m`j6{4jiJ*C!oVA8ZC?$1RM#K(_b zL9TW)kN*Y4%^-qPpMP7d4)o?Nk#>aoYHT(*g)qmRUb?**F@pnNiy6Fv9rEiUqD(^O zzyS?nBrX63BTRYduaG(0VVG2yJRe%o&rVrLjbxTaAFTd8s;<<@Qs>u(<193R8>}2_ zuwp{7;H2a*X7_jryzriZXMg?bTuegABb^87@SsKkr2)0Gyiax8KQWstw^v#ix45EVrcEhr>!NMhprl$InQMzjSFH54x5k9qHc`@9uKQzvL4ihcq{^B zPrVR=o_ic%Y>6&rMN)hTZsI7I<3&`#(nl+3y3ys9A~&^=4?PL&nd8)`OfG#n zwAMN$1&>K++c{^|7<4P=2y(B{jJsQ0a#U;HTo4ZmWZYvI{+s;Td{Yzem%0*k#)vjpB zia;J&>}ICate44SFYY3vEelqStQWFihx%^vQ@Do(sOy7yR2@WNv7Y9I^yL=nZr3mb zXKV5t@=?-Sk|b{XMhA7ZGB@2hqsx}4xwCW!in#C zI@}scZlr3-NFJ@NFaJlhyfcw{k^vvtGl`N9xSo**rDW4S}i zM9{fMPWo%4wYDG~BZ18BD+}h|GQKc-g^{++3MY>}W_uq7jGHx{mwE9fZiPCoxN$+7 zrODGGJrOkcPQUB(FD5aoS4g~7#6NR^ma7-!>mHuJfY5kTe6PpNNKC9GGRiu^L31uG z$7v`*JknQHsYB!Tm_W{a32TM099djW%5e+j0Ve_ct}IM>XLF1Ap+YvcrLV=|CKo6S zb+9Nl3_YdKP6%Cxy@6TxZ>;4&nTneadr z_ES90ydCev)LV!dN=#(*f}|ZORFdvkYBni^aLbUk>BajeWIOcmHP#8S)*2U~QKI%S zyrLmtPqb&TphJ;>yAxri#;{uyk`JJqODDw%(Z=2`1uc}br^V%>j!gS)D*q*f_-qf8&D;W1dJgQMlaH5er zN2U<%Smb7==vE}dDI8K7cKz!vs^73o9f>2sgiTzWcwY|BMYHH5%Vn7#kiw&eItCqa zIkR2~Q}>X=Ar8W|^Ms41Fm8o6IB2_j60eOeBB1Br!boW7JnoeX6Gs)?7rW0^5psc- zjS16yb>dFn>KPOF;imD}e!enuIniFzv}n$m2#gCCv4jM#ArwlzZ$7@9&XkFxZ4n!V zj3dyiwW4Ki2QG{@i>yuZXQizw_OkZI^-3otXC{!(lUpJF33gI60ak;Uqitp74|B6I zgg{b=Iz}WkhCGj1M=hu4#Aw173YxIVbISaoc z-nLZC*6Tgivd5V`K%GxhBsp@SUU60-rfc$=wb>zdJzXS&-5(NRRodFk;Kxk!S(O(a0e7oY=E( zAyS;Ow?6Q&XA+cnkCb{28_1N8H#?J!*$MmIwLq^*T_9-z^&UE@A(z9oGYtFy6EZef LrJugUA?W`A8`#=m literal 0 HcmV?d00001 diff --git a/how-is-this-not-illegal/sql/destroy.sql b/how-is-this-not-illegal/sql/destroy.sql new file mode 100644 index 0000000..2eb0b07 --- /dev/null +++ b/how-is-this-not-illegal/sql/destroy.sql @@ -0,0 +1,2 @@ +-- pokemon table for postgresql +DROP TABLE pokemon; diff --git a/how-is-this-not-illegal/sql/init.sql b/how-is-this-not-illegal/sql/init.sql new file mode 100644 index 0000000..7133119 --- /dev/null +++ b/how-is-this-not-illegal/sql/init.sql @@ -0,0 +1,159 @@ +-- pokemon table for postgresql +CREATE TABLE pokemon ( + id INT PRIMARY KEY, + name VARCHAR(50) NOT NULL, + type VARCHAR(50) NOT NULL +); + +INSERT INTO pokemon (id, name, type) VALUES +(1, 'Bulbasaur', 'Grass'), +(2, 'Ivysaur', 'Grass'), +(3, 'Venusaur', 'Grass'), +(4, 'Charmander', 'Fire'), +(5, 'Charmeleon', 'Fire'), +(6, 'Charizard', 'Fire'), +(7, 'Squirtle', 'Water'), +(8, 'Wartortle', 'Water'), +(9, 'Blastoise', 'Water'), +(10, 'Caterpie', 'Bug'), +(11, 'Metapod', 'Bug'), +(12, 'Butterfree', 'Bug'), +(13, 'Weedle', 'Bug'), +(14, 'Kakuna', 'Bug'), +(15, 'Beedrill', 'Bug'), +(16, 'Pidgey', 'Normal'), +(17, 'Pidgeotto', 'Normal'), +(18, 'Pidgeot', 'Normal'), +(19, 'Rattata', 'Normal'), +(20, 'Raticate', 'Normal'), +(21, 'Spearow', 'Normal'), +(22, 'Fearow', 'Normal'), +(23, 'Ekans', 'Poison'), +(24, 'Arbok', 'Poison'), +(25, 'Pikachu', 'Electric'), +(26, 'Raichu', 'Electric'), +(27, 'Sandshrew', 'Ground'), +(28, 'Sandslash', 'Ground'), +(29, 'Nidoran♀', 'Poison'), +(30, 'Nidorina', 'Poison'), +(31, 'Nidoqueen', 'Poison'), +(32, 'Nidoran♂', 'Poison'), +(33, 'Nidorino', 'Poison'), +(34, 'Nidoking', 'Poison'), +(35, 'Clefairy', 'Fairy'), +(36, 'Clefable', 'Fairy'), +(37, 'Vulpix', 'Fire'), +(38, 'Ninetales', 'Fire'), +(39, 'Jigglypuff', 'Normal'), +(40, 'Wigglytuff', 'Normal'), +(41, 'Zubat', 'Poison'), +(42, 'Golbat', 'Poison'), +(43, 'Oddish', 'GrassPoison'), +(44, 'Gloom', 'GrassPoison'), +(45, 'Vileplume', 'GrassPoison'), +(46, 'Paras', 'BugGrass'), +(47, 'Parasect', 'BugGrass'), +(48, 'Venonat', 'BugPoison'), +(49, 'Venomoth', 'BugPoison'), +(50, 'Diglett', 'Ground'), +(51, 'Dugtrio', 'Ground'), +(52, 'Meowth', 'Normal'), +(53, 'Persian', 'Normal'), +(54, 'Psyduck', 'Water'), +(55, 'Golduck', 'Water'), +(56, 'Mankey', 'Fighting'), +(57, 'Primeape', 'Fighting'), +(58, 'Growlithe', 'Fire'), +(59, 'Arcanine', 'Fire'), +(60, 'Poliwag', 'Water'), +(61, 'Poliwhirl', 'Water'), +(62, 'Poliwrath', 'WaterFighting'), +(63, 'Abra', 'Psychic'), +(64, 'Kadabra', 'Psychic'), +(65, 'Alakazam', 'Psychic'), +(66, 'Machop', 'Fighting'), +(67, 'Machoke', 'Fighting'), +(68, 'Machamp', 'Fighting'), +(69, 'Bellsprout', 'GrassPoison'), +(70, 'Weepinbell', 'GrassPoison'), +(71, 'Victreebel', 'GrassPoison'), +(72, 'Tentacool', 'WaterPoison'), +(73, 'Tentacruel', 'WaterPoison'), +(74, 'Geodude', 'RockGround'), +(75, 'Graveler', 'RockGround'), +(76, 'Golem', 'RockGround'), +(77, 'Ponyta', 'Fire'), +(78, 'Rapidash', 'Fire'), +(79, 'Slowpoke', 'WaterPsychic'), +(80, 'Slowbro', 'WaterPsychic'), +(81, 'Magnemite', 'ElectricSteel'), +(82, 'Magneton', 'ElectricSteel'), +(83, 'Farfetch''d', 'NormalFlying'), +(84, 'Doduo', 'NormalFlying'), +(85, 'Dodrio', 'NormalFlying'), +(86, 'Seel', 'Water'), +(87, 'Dewgong', 'WaterIce'), +(88, 'Grimer', 'Poison'), +(89, 'Muk', 'Poison'), +(90, 'Shellder', 'Water'), +(91, 'Cloyster', 'WaterIce'), +(92, 'Gastly', 'GhostPoison'), +(93, 'Haunter', 'GhostPoison'), +(94, 'Gengar', 'GhostPoison'), +(95, 'Onix', 'RockGround'), +(96, 'Drowzee', 'Psychic'), +(97, 'Hypno', 'Psychic'), +(98, 'Krabby', 'Water'), +(99, 'Kingler', 'Water'), +(100, 'Voltorb', 'Electric'), +(101, 'Electrode', 'Electric'), +(102, 'Exeggcute', 'GrassPsychic'), +(103, 'Exeggutor', 'GrassPsychic'), +(104, 'Cubone', 'Ground'), +(105, 'Marowak', 'Ground'), +(106, 'Hitmonlee', 'Fighting'), +(107, 'Hitmonchan', 'Fighting'), +(108, 'Lickitung', 'Normal'), +(109, 'Koffing', 'Poison'), +(110, 'Weezing', 'Poison'), +(111, 'Rhyhorn', 'GroundRock'), +(112, 'Rhydon', 'GroundRock'), +(113, 'Chansey', 'Normal'), +(114, 'Tangela', 'Grass'), +(115, 'Kangaskhan', 'Normal'), +(116, 'Horsea', 'Water'), +(117, 'Seadra', 'Water'), +(118, 'Goldeen', 'Water'), +(119, 'Seaking', 'Water'), +(120, 'Staryu', 'Water'), +(121, 'Starmie', 'WaterPsychic'), +(122, 'Mr. Mime', 'PsychicFairy'), +(123, 'Scyther', 'BugFlying'), +(124, 'Jynx', 'IcePsychic'), +(125, 'Electabuzz', 'Electric'), +(126, 'Magmar', 'Fire'), +(127, 'Pinsir', 'Bug'), +(128, 'Tauros', 'Normal'), +(129, 'Magikarp', 'Water'), +(130, 'Gyarados', 'WaterFlying'), +(131, 'Lapras', 'WaterIce'), +(132, 'Ditto', 'Normal'), +(133, 'Eevee', 'Normal'), +(134, 'Vaporeon', 'Water'), +(135, 'Jolteon', 'Electric'), +(136, 'Flareon', 'Fire'), +(137, 'Porygon', 'Normal'), +(138, 'Omanyte', 'RockWater'), +(139, 'Omastar', 'RockWater'), +(140, 'Kabuto', 'RockWater'), +(141, 'Kabutops', 'RockWater'), +(142, 'Aerodactyl', 'RockFlying'), +(143, 'Snorlax', 'Normal'), +(144, 'Articuno', 'IceFlying'), +(145, 'Zapdos', 'ElectricFlying'), +(146, 'Moltres', 'FireFlying'), +(147, 'Dratini', 'Dragon'), +(148, 'Dragonair', 'Dragon'), +(149, 'Dragonite', 'DragonFlying'), +(150, 'Mewtwo', 'Psychic'), +(151, 'Mew', 'Psychic'); diff --git a/how-is-this-not-illegal/tsconfig.json b/how-is-this-not-illegal/tsconfig.json new file mode 100644 index 0000000..52ca40b --- /dev/null +++ b/how-is-this-not-illegal/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "noImplicitAny": false, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/how-is-this-not-illegal/vite.config.ts b/how-is-this-not-illegal/vite.config.ts new file mode 100644 index 0000000..0a2e98a --- /dev/null +++ b/how-is-this-not-illegal/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vite"; +import { spiceflowPlugin } from "spiceflow/dist/vite"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + clearScreen: false, + plugins: [ + // inspect(), + spiceflowPlugin({ + entry: "./app/main.tsx", + }), + tailwindcss(), + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3aa8b2..263f4fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,7 @@ importers: version: 10.1.3 prettier: specifier: ^3.4.2 - version: 3.4.2 + version: 3.5.0 spiceflow: specifier: workspace:* version: link:spiceflow @@ -44,7 +44,7 @@ importers: version: 1.50.1 '@tailwindcss/vite': specifier: ^4.0.5 - version: 4.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + version: 4.0.6(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -62,7 +62,7 @@ importers: version: link:../spiceflow tailwindcss: specifier: ^4.0.5 - version: 4.0.5 + version: 4.0.6 vite: specifier: ^6.1.0 version: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) @@ -71,6 +71,51 @@ importers: specifier: ^10.1.1 version: 10.1.1(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + how-is-this-not-illegal: + dependencies: + '@fontsource/inter': + specifier: ^4.5.15 + version: 4.5.15 + '@fontsource/roboto-mono': + specifier: ^4.5.10 + version: 4.5.10 + '@tailwindcss/vite': + specifier: ^4.0.6 + version: 4.0.6(vite@6.1.0(@types/node@18.16.3)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) + '@types/node': + specifier: 18.16.3 + version: 18.16.3 + '@types/react': + specifier: 19.0.8 + version: 19.0.8 + '@types/react-dom': + specifier: 19.0.3 + version: 19.0.3(@types/react@19.0.8) + '@vercel/postgres': + specifier: 0.4.1 + version: 0.4.1 + dotenv-cli: + specifier: ^8.0.0 + version: 8.0.0 + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + spiceflow: + specifier: workspace:* + version: link:../spiceflow + tailwindcss: + specifier: 4.0.6 + version: 4.0.6 + typescript: + specifier: 5.7.3 + version: 5.7.3 + vite: + specifier: ^6.1.0 + version: 6.1.0(@types/node@18.16.3)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + openapi-schema-diff: dependencies: json-schema-ref-resolver: @@ -78,7 +123,7 @@ importers: version: 2.0.1 semver: specifier: ^7.6.3 - version: 7.6.3 + version: 7.7.1 devDependencies: '@types/node': specifier: ^22.13.1 @@ -88,10 +133,10 @@ importers: version: 10.1.3 eslint: specifier: ^9.19.0 - version: 9.19.0(jiti@2.4.2) + version: 9.20.0(jiti@2.4.2) neostandard: specifier: ^0.12.0 - version: 0.12.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + version: 0.12.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) typescript: specifier: ^5.7.3 version: 5.7.3 @@ -109,10 +154,10 @@ importers: version: 1.1.9(zod@3.24.1) '@fastify/deepmerge': specifier: ^2.0.1 - version: 2.0.1 + version: 2.0.2 ai: specifier: ^4.1.17 - version: 4.1.17(react@19.0.0)(zod@3.24.1) + version: 4.1.34(react@19.0.0)(zod@3.24.1) camelcase: specifier: ^8.0.0 version: 8.0.0 @@ -148,7 +193,7 @@ importers: version: 23.0.171 semver: specifier: ^7.6.3 - version: 7.6.3 + version: 7.7.1 string-dedent: specifier: ^3.0.1 version: 3.0.1 @@ -174,15 +219,12 @@ importers: '@hiogawa/transforms': specifier: ^0.0.0 version: 0.0.0 - '@jacob-ebey/vite-react-server-dom': - specifier: ^0.0.12 - version: 0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) '@modelcontextprotocol/sdk': specifier: ^1.0.4 - version: 1.0.4 + version: 1.4.1 '@sinclair/typebox': specifier: ^0.34.14 - version: 0.34.15 + version: 0.34.16 '@vitejs/plugin-react': specifier: ^4.3.4 version: 4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) @@ -221,7 +263,7 @@ importers: version: 0.0.4 spiceflow: specifier: '*' - version: 1.6.1(@modelcontextprotocol/sdk@1.0.4) + version: 1.6.1(@modelcontextprotocol/sdk@1.4.1) superjson: specifier: ^2.2.2 version: 2.2.2 @@ -276,10 +318,10 @@ importers: version: 3.1.0(acorn@8.14.0)(rollup@4.34.6) '@remix-run/cloudflare': specifier: ^2.15.3 - version: 2.15.3(@cloudflare/workers-types@4.20250129.0)(typescript@5.7.3) + version: 2.15.3(@cloudflare/workers-types@4.20250204.0)(typescript@5.7.3) '@remix-run/cloudflare-pages': specifier: ^2.15.3 - version: 2.15.3(@cloudflare/workers-types@4.20250129.0)(typescript@5.7.3) + version: 2.15.3(@cloudflare/workers-types@4.20250204.0)(typescript@5.7.3) '@remix-run/react': specifier: ^2.15.3 version: 2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3) @@ -297,7 +339,7 @@ importers: version: 4.4.0 miniflare: specifier: ^3.20240404.0 - version: 3.20250129.0 + version: 3.20250204.0(bufferutil@4.0.9) react: specifier: 19.0.0 version: 19.0.0 @@ -319,10 +361,10 @@ importers: devDependencies: '@cloudflare/workers-types': specifier: ^4.20240502.0 - version: 4.20250129.0 + version: 4.20250204.0 '@remix-run/dev': specifier: ^2.15.3 - version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0)) + version: 2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.1)(bufferutil@4.0.9)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.108.0(@cloudflare/workers-types@4.20250204.0)(bufferutil@4.0.9)) '@types/react': specifier: ^19.0.8 version: 19.0.8 @@ -331,13 +373,13 @@ importers: version: 19.0.3(@types/react@19.0.8) autoprefixer: specifier: ^10.4.19 - version: 10.4.20(postcss@8.5.1) + version: 10.4.20(postcss@8.5.2) node-fetch: specifier: ^3.3.2 version: 3.3.2 postcss: specifier: ^8.4.38 - version: 8.5.1 + version: 8.5.2 rehype-mdx-import-media: specifier: ^1.2.0 version: 1.2.0 @@ -355,7 +397,7 @@ importers: version: 4.3.2(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) wrangler: specifier: ^3.48.0 - version: 3.107.2(@cloudflare/workers-types@4.20250129.0) + version: 3.108.0(@cloudflare/workers-types@4.20250204.0)(bufferutil@4.0.9) packages: @@ -396,8 +438,8 @@ packages: resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==} engines: {node: '>=18'} - '@ai-sdk/react@1.1.8': - resolution: {integrity: sha512-buHm7hP21xEOksnRQtJX9fKbi7cAUwanEBa5niddTDibCDKd+kIXP2vaJGy8+heB3rff+XSW3BWlA8pscK+n1g==} + '@ai-sdk/react@1.1.11': + resolution: {integrity: sha512-vfjZ7w2M+Me83HTMMrnnrmXotz39UDCMd27YQSrvt2f1YCLPloVpLhP+Y9TLZeFE/QiiRCrPYLDQm6aQJYJ9PQ==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -408,8 +450,8 @@ packages: zod: optional: true - '@ai-sdk/ui-utils@1.1.8': - resolution: {integrity: sha512-nbok53K1EalO2sZjBLFB33cqs+8SxiL6pe7ekZ7+5f2MJTwdvpShl6d9U4O8fO3DnZ9pYLzaVC0XNMxnJt030Q==} + '@ai-sdk/ui-utils@1.1.11': + resolution: {integrity: sha512-1SC9W4VZLcJtxHRv4Y0aX20EFeaEP6gUvVqoKLBBtMLOgtcZrv/F/HQRjGavGugiwlS3dsVza4X+E78fiwtlTA==} engines: {node: '>=18'} peerDependencies: zod: ^3.0.0 @@ -674,38 +716,38 @@ packages: resolution: {integrity: sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==} engines: {node: '>=16.13'} - '@cloudflare/workerd-darwin-64@1.20250129.0': - resolution: {integrity: sha512-M+xETVnl+xy2dfDDWmp0XXr2rttl70a6bljQygl0EmYmNswFTcYbQWCaBuNBo9kabU59rLKr4a/b3QZ07NoL/g==} + '@cloudflare/workerd-darwin-64@1.20250204.0': + resolution: {integrity: sha512-HpsgbWEfvdcwuZ8WAZhi1TlSCyyHC3tbghpKsOqGDaQNltyAFAWqa278TPNfcitYf/FmV4961v3eqUE+RFdHNQ==} engines: {node: '>=16'} cpu: [x64] os: [darwin] - '@cloudflare/workerd-darwin-arm64@1.20250129.0': - resolution: {integrity: sha512-c4PQUyIMp+bCMxZkAMBzXgTHjRZxeYCujDbb3staestqgRbenzcfauXsMd6np35ng+EE1uBgHNPV4+7fC0ZBfg==} + '@cloudflare/workerd-darwin-arm64@1.20250204.0': + resolution: {integrity: sha512-AJ8Tk7KMJqePlch3SH8oL41ROtsrb07hKRHD6M+FvGC3tLtf26rpteAAMNYKMDYKzFNFUIKZNijYDFZjBFndXQ==} engines: {node: '>=16'} cpu: [arm64] os: [darwin] - '@cloudflare/workerd-linux-64@1.20250129.0': - resolution: {integrity: sha512-xJx8LwWFxsm5U3DETJwRuOmT5RWBqm4FmA4itYXvcEICca9pWJDB641kT4PnpypwDNmYOebhU7A+JUrCRucG0w==} + '@cloudflare/workerd-linux-64@1.20250204.0': + resolution: {integrity: sha512-RIUfUSnDC8h73zAa+u1K2Frc7nc+eeQoBBP7SaqsRe6JdX8jfIv/GtWjQWCoj8xQFgLvhpJKZ4sTTTV+AilQbw==} engines: {node: '>=16'} cpu: [x64] os: [linux] - '@cloudflare/workerd-linux-arm64@1.20250129.0': - resolution: {integrity: sha512-dR//npbaX5p323huBVNIy5gaWubQx6CC3aiXeK0yX4aD5ar8AjxQFb2U/Sgjeo65Rkt53hJWqC7IwRpK/eOxrA==} + '@cloudflare/workerd-linux-arm64@1.20250204.0': + resolution: {integrity: sha512-8Ql8jDjoIgr2J7oBD01kd9kduUz60njofrBpAOkjCPed15He8e8XHkYaYow3g0xpae4S2ryrPOeoD3M64sRxeg==} engines: {node: '>=16'} cpu: [arm64] os: [linux] - '@cloudflare/workerd-windows-64@1.20250129.0': - resolution: {integrity: sha512-OeO+1nPj/ocAE3adFar/tRFGRkbCrBnrOYXq0FUBSpyNHpDdA9/U3PAw5CN4zvjfTnqXZfTxTFeqoruqzRzbtg==} + '@cloudflare/workerd-windows-64@1.20250204.0': + resolution: {integrity: sha512-RpDJO3+to+e17X3EWfRCagboZYwBz2fowc+jL53+fd7uD19v3F59H48lw2BDpHJMRyhg6ouWcpM94OhsHv8ecA==} engines: {node: '>=16'} cpu: [x64] os: [win32] - '@cloudflare/workers-types@4.20250129.0': - resolution: {integrity: sha512-H7g/sDB9GaV+fIPf3utNEYncFhryIvDThiBbfZtu0bZmVXcVd9ApP3OMqUYhNV8MShWQASvgWletKKBZGT9/oA==} + '@cloudflare/workers-types@4.20250204.0': + resolution: {integrity: sha512-mWoQbYaP+nYztx9I7q9sgaiNlT54Cypszz0RfzMxYnT5W3NXDuwGcjGB+5B5H5VB8tEC2dYnBRpa70lX94ueaQ==} '@code-hike/lighter@0.7.0': resolution: {integrity: sha512-64O07rIORKQLB+5T/GKAmKcD9sC0N9yHFJXa0Hs+0Aee1G+I4bSXxTccuDFP6c/G/3h5Pk7yv7PoX9/SpzaeiQ==} @@ -719,6 +761,9 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + '@emotion/hash@0.9.2': resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} @@ -1446,12 +1491,16 @@ packages: resolution: {integrity: sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.11.0': + resolution: {integrity: sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@3.2.0': resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.19.0': - resolution: {integrity: sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==} + '@eslint/js@9.20.0': + resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.5': @@ -1466,8 +1515,14 @@ packages: resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} - '@fastify/deepmerge@2.0.1': - resolution: {integrity: sha512-hx+wJQr9Ph1hY/dyzY0SxqjumMyqZDlIF6oe71dpRKDHUg7dFQfjG94qqwQ274XRjmUrwKiYadex8XplNHx3CA==} + '@fastify/deepmerge@2.0.2': + resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} + + '@fontsource/inter@4.5.15': + resolution: {integrity: sha512-FzleM9AxZQK2nqsTDtBiY0PMEVWvnKnuu2i09+p6DHvrHsuucoV2j0tmw+kAT3L4hvsLdAIDv6MdGehsPIdT+Q==} + + '@fontsource/roboto-mono@4.5.10': + resolution: {integrity: sha512-KrJdmkqz6DszT2wV/bbhXef4r0hV3B0vw2mAqei8A2kRnvq+gcJLmmIeQ94vu9VEXrUQzos5M9lH1TAAXpRphw==} '@glideapps/ts-necessities@2.2.3': resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==} @@ -1501,6 +1556,111 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1516,19 +1676,6 @@ packages: react: ^19.0.0 react-dom: ^19.0.0 - '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15': - resolution: {integrity: sha512-RTrrqEpaiWunZCLBQrsA+rZFNYSTjo/XiV5Uti21bx/IlEHQ6o8TyUxHxeoHPm0jTm/uRgxXVe46EwbzR7ywuw==} - engines: {node: '>=0.10.0'} - peerDependencies: - react: ^19.0.0 - react-dom: ^19.0.0 - - '@jacob-ebey/vite-react-server-dom@0.0.12': - resolution: {integrity: sha512-XzaqcFlnXgRv2ZVyHJEhJGLvTsx0XCpnGqlHywzrp6XjUMujyRvEdNp4EGt8VGpGtH0o+TlS6YxLTc8MVH3jGQ==} - peerDependencies: - '@jacob-ebey/react-server-dom-vite': '*' - vite: ^6.0.0 - '@jridgewell/gen-mapping@0.3.8': resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} @@ -1587,11 +1734,12 @@ packages: resolution: {integrity: sha512-mdvS1spIxmZoUbTdYmWknHtwm72WwrGNoQCDd4RTvcXJ9G6XThxeC3g+cpOf6Fw6vIERHt50pYiJpsk5XTJQ5w==} engines: {node: '>=8'} - '@mjackson/node-fetch-server@0.5.0': - resolution: {integrity: sha512-GZrkGuP3N7he0GdK9CCqpjabqsXjJa4tp0yKw973FoGAAOGE6WTcp3kcosRdeGYqtoFn7IEu84g3pItk4wRBFg==} + '@modelcontextprotocol/sdk@1.4.1': + resolution: {integrity: sha512-wS6YC4lkUZ9QpP+/7NBTlVNiEvsnyl0xF7rRusLF+RsG0xDPc/zWR7fEEyhKnnNutGsDAZh59l/AeoWGwIb1+g==} + engines: {node: '>=18'} - '@modelcontextprotocol/sdk@1.0.4': - resolution: {integrity: sha512-C+jw1lF6HSGzs7EZpzHbXfzz9rj9him4BaoumlTciW/IDDgIpweF/qiCWKlP02QKg5PPcgY6xY2WCt5y2tpYow==} + '@neondatabase/serverless@0.5.6': + resolution: {integrity: sha512-Ru0lG6W/nQtHRkDFVQFF+1PJYx8wd3jereln0Ep0YkiHey50hjTLVUycQoE4X977605pXMuFWORweuktzph+Xg==} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1938,8 +2086,8 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.34.15': - resolution: {integrity: sha512-xeIzl3h1Znn9w/LTITqpiwag0gXjA+ldi2ZkXIBxGEppGCW211Tza+eL6D4pKqs10bj5z2umBWk5WL6spQ2OCQ==} + '@sinclair/typebox@0.34.16': + resolution: {integrity: sha512-rIljj8VPYAfn26ANY+5pCNVBPiv6hSufuKGe46y65cJZpvx8vHvPXlU0Q/Le4OGtlNaL8Jg2FuhtvQX18lSIqA==} '@stefanprobst/rehype-extract-toc@2.2.1': resolution: {integrity: sha512-SfDrnqz7WVp/xYxPqAxD4lR/CJZcsFcy1T0JNAZfK4grdHJAbHplhF5yZgAOnba5+7ovbpRwfHMffTFlrcvwFQ==} @@ -1951,77 +2099,77 @@ packages: peerDependencies: eslint: '>=8.40.0' - '@tailwindcss/node@4.0.5': - resolution: {integrity: sha512-ffTz4DX1cgr4XPuqjhm32YV6Lyx58R1CxAAnSFTamg6wXwfk3oWdb6exgAbGesPzvUgicTO0gwUdQGSsg4nNog==} + '@tailwindcss/node@4.0.6': + resolution: {integrity: sha512-jb6E0WeSq7OQbVYcIJ6LxnZTeC4HjMvbzFBMCrQff4R50HBlo/obmYNk6V2GCUXDeqiXtvtrQgcIbT+/boB03Q==} - '@tailwindcss/oxide-android-arm64@4.0.5': - resolution: {integrity: sha512-kK/ik8aIAKWDIEYDZGUCJcnU1qU5sPoMBlVzPvtsUqiV6cSHcnVRUdkcLwKqTeUowzZtjjRiamELLd9Gb0x5BQ==} + '@tailwindcss/oxide-android-arm64@4.0.6': + resolution: {integrity: sha512-xDbym6bDPW3D2XqQqX3PjqW3CKGe1KXH7Fdkc60sX5ZLVUbzPkFeunQaoP+BuYlLc2cC1FoClrIRYnRzof9Sow==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.0.5': - resolution: {integrity: sha512-vkbXFv0FfAEbrSa5NBjFEE+xi06ha7mxuxjY8LRn7d7/tBGrAZOEJnnsEbB6M1+x2pGRTjjei0XyTIXdVCglJA==} + '@tailwindcss/oxide-darwin-arm64@4.0.6': + resolution: {integrity: sha512-1f71/ju/tvyGl5c2bDkchZHy8p8EK/tDHCxlpYJ1hGNvsYihZNurxVpZ0DefpN7cNc9RTT8DjrRoV8xXZKKRjg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.0.5': - resolution: {integrity: sha512-PedA64rHBXEa4e6abBWE4Yj4gHulfPb5T+rBNnX+WGkjjge5Txa2oS99TLmJ5BPDkXXqz/Ba7oweWIDDG7i5NQ==} + '@tailwindcss/oxide-darwin-x64@4.0.6': + resolution: {integrity: sha512-s/hg/ZPgxFIrGMb0kqyeaqZt505P891buUkSezmrDY6lxv2ixIELAlOcUVTkVh245SeaeEiUVUPiUN37cwoL2g==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.0.5': - resolution: {integrity: sha512-silz3nuZdEYDfic3v/ooVUQChj9hbxDSee43GCQNwr/iD9L4K/JsZtoNqr0w69pUkvWcKINOGOG0r7WqUqkAeg==} + '@tailwindcss/oxide-freebsd-x64@4.0.6': + resolution: {integrity: sha512-Z3Wo8FWZnmio8+xlcbb7JUo/hqRMSmhQw8IGIRoRJ7GmLR0C+25Wq+bEX/135xe/yEle2lFkhu9JBHd4wZYiig==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5': - resolution: {integrity: sha512-ElneG75XS64B9I2G83A/Hc7EtNVOD5xahs7avq0aeW7mEX6CtMc8m8RCXMn3jGhz8enFE52l6QU0wO7iVkEtXQ==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6': + resolution: {integrity: sha512-SNSwkkim1myAgmnbHs4EjXsPL7rQbVGtjcok5EaIzkHkCAVK9QBQsWeP2Jm2/JJhq4wdx8tZB9Y7psMzHYWCkA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.0.5': - resolution: {integrity: sha512-8yoXpWTeIFaByUaKy2qRAppznLVaDHP9xYCAbS3FG7+uUwHi8CHE4TcomM7eyamo0U7dbUIDgKMGoAX5s2iVrA==} + '@tailwindcss/oxide-linux-arm64-gnu@4.0.6': + resolution: {integrity: sha512-tJ+mevtSDMQhKlwCCuhsFEFg058kBiSy4TkoeBG921EfrHKmexOaCyFKYhVXy4JtkaeeOcjJnCLasEeqml4i+Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.0.5': - resolution: {integrity: sha512-BDlVSiiJ08GRz9KKnXgaPFs2fkukPF3pym6uK3oWEKW45jKlVGgybLqulcV5nLEqREOuyq4Rn4vnZss4/bbQ/g==} + '@tailwindcss/oxide-linux-arm64-musl@4.0.6': + resolution: {integrity: sha512-IoArz1vfuTR4rALXMUXI/GWWfx2EaO4gFNtBNkDNOYhlTD4NVEwE45nbBoojYiTulajI4c2XH8UmVEVJTOJKxA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.0.5': - resolution: {integrity: sha512-DYgieNDRkTy69bWPgdsc47nAXa74P63P/RetUwYM9vYj5USyOfHCEcqIthkCuYw3dXKBhjgwe697TmL2g2jpAw==} + '@tailwindcss/oxide-linux-x64-gnu@4.0.6': + resolution: {integrity: sha512-QtsUfLkEAeWAC3Owx9Kg+7JdzE+k9drPhwTAXbXugYB9RZUnEWWx5x3q/au6TvUYcL+n0RBqDEO2gucZRvRFgQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.0.5': - resolution: {integrity: sha512-z2RzUvOQl0ZqrZqmCFP53tJbBXQ3UmLD/E6J7+q0e+4VaFnXCcIYTfQbHgI8f3fash+q6gK80Ko/ywEQ+bvv6Q==} + '@tailwindcss/oxide-linux-x64-musl@4.0.6': + resolution: {integrity: sha512-QthvJqIji2KlGNwLcK/PPYo7w1Wsi/8NK0wAtRGbv4eOPdZHkQ9KUk+oCoP20oPO7i2a6X1aBAFQEL7i08nNMA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-win32-arm64-msvc@4.0.5': - resolution: {integrity: sha512-ho1dJ4o5Q8nAOxdMkbfBu5aSqI+/bzQ0jEeHcXaEdEJzf2fSWs3HY7bIKtE6vQS8c4SmSBvls7IhGPuJxNg+2Q==} + '@tailwindcss/oxide-win32-arm64-msvc@4.0.6': + resolution: {integrity: sha512-+oka+dYX8jy9iP00DJ9Y100XsqvbqR5s0yfMZJuPR1H/lDVtDfsZiSix1UFBQ3X1HWxoEEl6iXNJHWd56TocVw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.0.5': - resolution: {integrity: sha512-yjw6JhtyDXr+G0aZrj3L3NlEV7CobSqOdPyfo6G3d91WEZ5b8PyGm86IAreX08Jp9DChGXEd53gWysVpWCTs+w==} + '@tailwindcss/oxide-win32-x64-msvc@4.0.6': + resolution: {integrity: sha512-+o+juAkik4p8Ue/0LiflQXPmVatl6Av3LEZXpBTfg4qkMIbZdhCGWFzHdt2NjoMiLOJCFDddoV6GYaimvK1Olw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.0.5': - resolution: {integrity: sha512-iWGyOCu0TuzvCBisWbGv2K9+7QCfE0ztgtrZOvb9iF7V7ChVkD15Obe3HevZrhjngAc34jDA+OMSuSvkrpTy4A==} + '@tailwindcss/oxide@4.0.6': + resolution: {integrity: sha512-lVyKV2y58UE9CeKVcYykULe9QaE1dtKdxDEdrTPIdbzRgBk6bdxHNAoDqvcqXbIGXubn3VOl1O/CFF77v/EqSA==} engines: {node: '>= 10'} '@tailwindcss/typography@0.5.16': @@ -2029,8 +2177,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tailwindcss/vite@4.0.5': - resolution: {integrity: sha512-/i4hjLTUYVjUG0MTUviQP3HR/hzwyzv8Sq4sz2pnsNuf+FIjjhJB0vcnIMH1KIX0k8ozD6CBv2Dl76tlm/JFFA==} + '@tailwindcss/vite@4.0.6': + resolution: {integrity: sha512-O25vZ/URWbZ2JHdk2o8wH7jOKqEGCsYmX3GwGmYS5DjE4X3mpf93a72Rn7VRnefldNauBzr5z2hfZptmBNtTUQ==} peerDependencies: vite: ^5.2.0 || ^6 @@ -2121,9 +2269,15 @@ packages: '@types/node@16.18.126': resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==} + '@types/node@18.16.3': + resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} + '@types/node@22.13.1': resolution: {integrity: sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==} + '@types/pg@8.6.6': + resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==} + '@types/react-dom@19.0.3': resolution: {integrity: sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==} peerDependencies: @@ -2200,6 +2354,10 @@ packages: '@vanilla-extract/private@1.0.6': resolution: {integrity: sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw==} + '@vercel/postgres@0.4.1': + resolution: {integrity: sha512-rYlNnaXrr2/NWK/OodhAUyed0bomaizKKC8XXjNYv8I1K3m75oocP4IGTcBpZe76VCrHuaKW5d6jLQnuRRoNKg==} + engines: {node: '>=14.6'} + '@vitejs/plugin-react@4.3.4': resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} engines: {node: ^14.18.0 || >=16.0.0} @@ -2254,6 +2412,10 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + acorn-walk@8.3.4: resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} engines: {node: '>=0.4.0'} @@ -2267,8 +2429,8 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} - ai@4.1.17: - resolution: {integrity: sha512-5SW15tXDuxE/wlEOjRKxLxTOUIGD4C9bIee+FCFvXHTTAZhHiQjViC2s7RtMUW+hbFtGya302jUHY1Pe8A/YuQ==} + ai@4.1.34: + resolution: {integrity: sha512-9IB5duz6VbXvjibqNrvKz6++PwE8Ui5UfbOC9/CtcQN5Z9sudUQErss+maj7ptoPysD2NPjj99e0Hp183Cz5LQ==} engines: {node: '>=18'} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -2454,6 +2616,14 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bufferutil@4.0.7: + resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} + engines: {node: '>=6.14.2'} + + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} + engines: {node: '>=6.14.2'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -2593,6 +2763,13 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -2635,6 +2812,10 @@ packages: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -2643,10 +2824,6 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} @@ -2796,6 +2973,10 @@ packages: engines: {node: '>=0.10'} hasBin: true + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2832,6 +3013,14 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dotenv-cli@8.0.0: + resolution: {integrity: sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==} + hasBin: true + + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + dotenv@16.4.7: resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} engines: {node: '>=12'} @@ -2873,6 +3062,10 @@ packages: resolution: {integrity: sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} + enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} @@ -3033,8 +3226,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.19.0: - resolution: {integrity: sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==} + eslint@9.20.0: + resolution: {integrity: sha512-aL4F8167Hg4IvsW89ejnpTwx+B/UQRzJPGgbIOl+4XqffWsahVVsLEWoZvnrVuwpWmnRd7XeXmQI1zlKcFDteA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -3551,6 +3744,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-async-function@2.1.0: resolution: {integrity: sha512-GExz9MtyhlZyXYLxzlJRj5WUCE661zhDa1Yna52CN57AJsymh+DvXXjyveSioqSRdxvUrdKdvqB1b5cVKsNpWQ==} engines: {node: '>= 0.4'} @@ -4290,8 +4486,8 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} - miniflare@3.20250129.0: - resolution: {integrity: sha512-qYlGEjMl/2kJdgNaztj4hpA64d6Dl79Lx/NL61p/v5XZRiWanBOTgkQqdPxCKZOj6KQnioqhC7lfd6jDXKSs2A==} + miniflare@3.20250204.0: + resolution: {integrity: sha512-f7tezEkOvVRVHIVul2EbTyKvWJCXpTDRAOxTxtD4N92+YI8PC2P8AvO4Z30vlN61r5Pje33fTBG8G1fEwSZIqQ==} engines: {node: '>=16.13'} hasBin: true @@ -4387,8 +4583,8 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} - neostandard@0.12.0: - resolution: {integrity: sha512-MvtiRhevDzE+oqQUxFvDsEmipzy3erNmnz5q5TG9M8xZ30n86rt4PxGP9jgocGIZr1105OgPZNlK2FQEtb2Vng==} + neostandard@0.12.1: + resolution: {integrity: sha512-As/LDK+xx591BLb1rPRaPs+JfXFgyNx5BoBui1KBeF/J4s0mW8+NBohrYnMfgm1w1t7E/Y/tU34MjMiP6lns6A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -4411,6 +4607,10 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -4649,6 +4849,17 @@ packages: periscopic@4.0.2: resolution: {integrity: sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==} + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.7.1: + resolution: {integrity: sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4782,6 +4993,26 @@ packages: resolution: {integrity: sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.2: + resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4791,8 +5022,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.4.2: - resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + prettier@3.5.0: + resolution: {integrity: sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==} engines: {node: '>=14'} hasBin: true @@ -5124,8 +5355,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.0: - resolution: {integrity: sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==} + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} engines: {node: '>=10'} hasBin: true @@ -5155,6 +5386,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -5189,6 +5424,9 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + sirv@3.0.0: resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} engines: {node: '>=18'} @@ -5373,8 +5611,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swr@2.3.0: - resolution: {integrity: sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==} + swr@2.3.2: + resolution: {integrity: sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==} peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -5387,8 +5625,8 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tailwindcss@4.0.5: - resolution: {integrity: sha512-DZZIKX3tA23LGTjHdnwlJOTxfICD1cPeykLLsYF1RQBI9QsCR3i0szohJfJDVjr6aNRAIio5WVO7FGB77fRHwg==} + tailwindcss@4.0.6: + resolution: {integrity: sha512-mysewHYJKaXgNOW6pp5xon/emCsfAMnO8WMaGKZZ35fomnR/T5gYnRg2/yRTTrtXiEl1tiVkeRt0eMO6HxEZqw==} tapable@2.2.1: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} @@ -5702,6 +5940,10 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utf-8-validate@6.0.3: + resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==} + engines: {node: '>=6.14.2'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5947,17 +6189,17 @@ packages: resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} engines: {node: '>=12.17'} - workerd@1.20250129.0: - resolution: {integrity: sha512-Rprz8rxKTF4l6q/nYYI07lBetJnR19mGipx+u/a27GZOPKMG5SLIzA2NciZlJaB2Qd5YY+4p/eHOeKqo5keVWA==} + workerd@1.20250204.0: + resolution: {integrity: sha512-zcKufjVFsQMiD3/acg1Ix00HIMCkXCrDxQXYRDn/1AIz3QQGkmbVDwcUk1Ki2jBUoXmBCMsJdycRucgMVEypWg==} engines: {node: '>=16'} hasBin: true - wrangler@3.107.2: - resolution: {integrity: sha512-YOSfx0pETj18qcBD4aLvHjlcoV1sCVxYm9En8YphymW5rlTYD0Lc4MR3kzN1AGiWCjJ/ydrvA7eZuF/zPBotiw==} + wrangler@3.108.0: + resolution: {integrity: sha512-w8J0VtDqn8F94qw+HnxFbri7MMdT/to5/w1QHAjR//tIHkilKAUFNaEF3GDEJREvUG3iHuawrH2p5ATTHnFc/Q==} engines: {node: '>=16.17.0'} hasBin: true peerDependencies: - '@cloudflare/workers-types': ^4.20250129.0 + '@cloudflare/workers-types': ^4.20250204.0 peerDependenciesMeta: '@cloudflare/workers-types': optional: true @@ -5985,6 +6227,18 @@ packages: utf-8-validate: optional: true + ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -6032,8 +6286,8 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - youch@3.3.4: - resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==} + youch@3.2.3: + resolution: {integrity: sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw==} zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} @@ -6043,6 +6297,9 @@ packages: peerDependencies: zod: ^3.24.1 + zod@3.22.3: + resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==} + zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} @@ -6090,17 +6347,17 @@ snapshots: dependencies: json-schema: 0.4.0 - '@ai-sdk/react@1.1.8(react@19.0.0)(zod@3.24.1)': + '@ai-sdk/react@1.1.11(react@19.0.0)(zod@3.24.1)': dependencies: '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - '@ai-sdk/ui-utils': 1.1.8(zod@3.24.1) - swr: 2.3.0(react@19.0.0) + '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) + swr: 2.3.2(react@19.0.0) throttleit: 2.1.0 optionalDependencies: react: 19.0.0 zod: 3.24.1 - '@ai-sdk/ui-utils@1.1.8(zod@3.24.1)': + '@ai-sdk/ui-utils@1.1.11(zod@3.24.1)': dependencies: '@ai-sdk/provider': 1.0.7 '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) @@ -6541,22 +6798,22 @@ snapshots: dependencies: mime: 3.0.0 - '@cloudflare/workerd-darwin-64@1.20250129.0': + '@cloudflare/workerd-darwin-64@1.20250204.0': optional: true - '@cloudflare/workerd-darwin-arm64@1.20250129.0': + '@cloudflare/workerd-darwin-arm64@1.20250204.0': optional: true - '@cloudflare/workerd-linux-64@1.20250129.0': + '@cloudflare/workerd-linux-64@1.20250204.0': optional: true - '@cloudflare/workerd-linux-arm64@1.20250129.0': + '@cloudflare/workerd-linux-arm64@1.20250204.0': optional: true - '@cloudflare/workerd-windows-64@1.20250129.0': + '@cloudflare/workerd-windows-64@1.20250204.0': optional: true - '@cloudflare/workers-types@4.20250129.0': {} + '@cloudflare/workers-types@4.20250204.0': {} '@code-hike/lighter@0.7.0': {} @@ -6572,6 +6829,11 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + '@emotion/hash@0.9.2': {} '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19)': @@ -6932,9 +7194,9 @@ snapshots: '@esbuild/win32-x64@0.24.2': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.19.0(jiti@2.4.2))': + '@eslint-community/eslint-utils@4.4.1(eslint@9.20.0(jiti@2.4.2))': dependencies: - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -6951,6 +7213,10 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.11.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 @@ -6965,7 +7231,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.19.0': {} + '@eslint/js@9.20.0': {} '@eslint/object-schema@2.1.5': {} @@ -6976,7 +7242,11 @@ snapshots: '@fastify/busboy@2.1.1': {} - '@fastify/deepmerge@2.0.1': {} + '@fastify/deepmerge@2.0.2': {} + + '@fontsource/inter@4.5.15': {} + + '@fontsource/roboto-mono@4.5.10': {} '@glideapps/ts-necessities@2.2.3': {} @@ -7003,6 +7273,81 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -7019,22 +7364,6 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) - '@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': - dependencies: - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - - '@jacob-ebey/vite-react-server-dom@0.0.12(@jacob-ebey/react-server-dom-vite@19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(rollup@4.34.6)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': - dependencies: - '@jacob-ebey/react-server-dom-vite': 19.0.0-experimental.15(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mjackson/node-fetch-server': 0.5.0 - '@vitejs/plugin-react': 4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0)) - unplugin-rsc: 0.0.11(rollup@4.34.6) - vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) - transitivePeerDependencies: - - rollup - - supports-color - '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 @@ -7168,13 +7497,17 @@ snapshots: dependencies: object-treeify: 1.1.33 - '@mjackson/node-fetch-server@0.5.0': {} - - '@modelcontextprotocol/sdk@1.0.4': + '@modelcontextprotocol/sdk@1.4.1': dependencies: content-type: 1.0.5 + eventsource: 3.0.5 raw-body: 3.0.0 zod: 3.24.1 + zod-to-json-schema: 3.24.1(zod@3.24.1) + + '@neondatabase/serverless@0.5.6': + dependencies: + '@types/pg': 8.6.6 '@nodelib/fs.scandir@2.1.5': dependencies: @@ -7192,7 +7525,7 @@ snapshots: '@npmcli/fs@3.1.1': dependencies: - semver: 7.7.0 + semver: 7.7.1 '@npmcli/git@4.1.0': dependencies: @@ -7202,7 +7535,7 @@ snapshots: proc-log: 3.0.0 promise-inflight: 1.0.1 promise-retry: 2.0.1 - semver: 7.7.0 + semver: 7.7.1 which: 3.0.1 transitivePeerDependencies: - bluebird @@ -7215,7 +7548,7 @@ snapshots: json-parse-even-better-errors: 3.0.2 normalize-package-data: 5.0.0 proc-log: 3.0.0 - semver: 7.7.0 + semver: 7.7.1 transitivePeerDependencies: - bluebird @@ -7234,22 +7567,22 @@ snapshots: '@polka/url@1.0.0-next.28': {} - '@remix-run/cloudflare-pages@2.15.3(@cloudflare/workers-types@4.20250129.0)(typescript@5.7.3)': + '@remix-run/cloudflare-pages@2.15.3(@cloudflare/workers-types@4.20250204.0)(typescript@5.7.3)': dependencies: - '@cloudflare/workers-types': 4.20250129.0 - '@remix-run/cloudflare': 2.15.3(@cloudflare/workers-types@4.20250129.0)(typescript@5.7.3) + '@cloudflare/workers-types': 4.20250204.0 + '@remix-run/cloudflare': 2.15.3(@cloudflare/workers-types@4.20250204.0)(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 - '@remix-run/cloudflare@2.15.3(@cloudflare/workers-types@4.20250129.0)(typescript@5.7.3)': + '@remix-run/cloudflare@2.15.3(@cloudflare/workers-types@4.20250204.0)(typescript@5.7.3)': dependencies: '@cloudflare/kv-asset-handler': 0.1.3 - '@cloudflare/workers-types': 4.20250129.0 + '@cloudflare/workers-types': 4.20250204.0 '@remix-run/server-runtime': 2.15.3(typescript@5.7.3) optionalDependencies: typescript: 5.7.3 - '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0))': + '@remix-run/dev@2.15.3(@remix-run/react@2.15.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.3))(@types/node@22.13.1)(bufferutil@4.0.9)(lightningcss@1.29.1)(terser@5.31.6)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3))(typescript@5.7.3)(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))(wrangler@3.108.0(@cloudflare/workers-types@4.20250204.0)(bufferutil@4.0.9))': dependencies: '@babel/core': 7.26.7 '@babel/generator': 7.26.5 @@ -7291,26 +7624,26 @@ snapshots: picocolors: 1.1.1 picomatch: 2.3.1 pidtree: 0.6.0 - postcss: 8.5.1 - postcss-discard-duplicates: 5.1.0(postcss@8.5.1) - postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) - postcss-modules: 6.0.1(postcss@8.5.1) + postcss: 8.5.2 + postcss-discard-duplicates: 5.1.0(postcss@8.5.2) + postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) + postcss-modules: 6.0.1(postcss@8.5.2) prettier: 2.8.8 pretty-ms: 7.0.1 react-refresh: 0.14.2 remark-frontmatter: 4.0.1 remark-mdx-frontmatter: 1.1.1 - semver: 7.7.0 + semver: 7.7.1 set-cookie-parser: 2.7.1 tar-fs: 2.1.2 tsconfig-paths: 4.2.0 valibot: 0.41.0(typescript@5.7.3) vite-node: 1.6.0(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6) - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.9) optionalDependencies: typescript: 5.7.3 vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) - wrangler: 3.107.2(@cloudflare/workers-types@4.20250129.0) + wrangler: 3.108.0(@cloudflare/workers-types@4.20250204.0)(bufferutil@4.0.9) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -7523,7 +7856,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.34.6': optional: true - '@sinclair/typebox@0.34.15': {} + '@sinclair/typebox@0.34.16': {} '@stefanprobst/rehype-extract-toc@2.2.1': dependencies: @@ -7533,10 +7866,10 @@ snapshots: hast-util-to-string: 2.0.0 unist-util-visit: 4.1.2 - '@stylistic/eslint-plugin@2.11.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': + '@stylistic/eslint-plugin@2.11.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - eslint: 9.19.0(jiti@2.4.2) + '@typescript-eslint/utils': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.20.0(jiti@2.4.2) eslint-visitor-keys: 4.2.0 espree: 10.3.0 estraverse: 5.3.0 @@ -7545,58 +7878,58 @@ snapshots: - supports-color - typescript - '@tailwindcss/node@4.0.5': + '@tailwindcss/node@4.0.6': dependencies: - enhanced-resolve: 5.18.0 + enhanced-resolve: 5.18.1 jiti: 2.4.2 - tailwindcss: 4.0.5 + tailwindcss: 4.0.6 - '@tailwindcss/oxide-android-arm64@4.0.5': + '@tailwindcss/oxide-android-arm64@4.0.6': optional: true - '@tailwindcss/oxide-darwin-arm64@4.0.5': + '@tailwindcss/oxide-darwin-arm64@4.0.6': optional: true - '@tailwindcss/oxide-darwin-x64@4.0.5': + '@tailwindcss/oxide-darwin-x64@4.0.6': optional: true - '@tailwindcss/oxide-freebsd-x64@4.0.5': + '@tailwindcss/oxide-freebsd-x64@4.0.6': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.5': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.0.6': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.0.5': + '@tailwindcss/oxide-linux-arm64-gnu@4.0.6': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.0.5': + '@tailwindcss/oxide-linux-arm64-musl@4.0.6': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.0.5': + '@tailwindcss/oxide-linux-x64-gnu@4.0.6': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.0.5': + '@tailwindcss/oxide-linux-x64-musl@4.0.6': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.0.5': + '@tailwindcss/oxide-win32-arm64-msvc@4.0.6': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.0.5': + '@tailwindcss/oxide-win32-x64-msvc@4.0.6': optional: true - '@tailwindcss/oxide@4.0.5': + '@tailwindcss/oxide@4.0.6': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.0.5 - '@tailwindcss/oxide-darwin-arm64': 4.0.5 - '@tailwindcss/oxide-darwin-x64': 4.0.5 - '@tailwindcss/oxide-freebsd-x64': 4.0.5 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.5 - '@tailwindcss/oxide-linux-arm64-gnu': 4.0.5 - '@tailwindcss/oxide-linux-arm64-musl': 4.0.5 - '@tailwindcss/oxide-linux-x64-gnu': 4.0.5 - '@tailwindcss/oxide-linux-x64-musl': 4.0.5 - '@tailwindcss/oxide-win32-arm64-msvc': 4.0.5 - '@tailwindcss/oxide-win32-x64-msvc': 4.0.5 + '@tailwindcss/oxide-android-arm64': 4.0.6 + '@tailwindcss/oxide-darwin-arm64': 4.0.6 + '@tailwindcss/oxide-darwin-x64': 4.0.6 + '@tailwindcss/oxide-freebsd-x64': 4.0.6 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.0.6 + '@tailwindcss/oxide-linux-arm64-gnu': 4.0.6 + '@tailwindcss/oxide-linux-arm64-musl': 4.0.6 + '@tailwindcss/oxide-linux-x64-gnu': 4.0.6 + '@tailwindcss/oxide-linux-x64-musl': 4.0.6 + '@tailwindcss/oxide-win32-arm64-msvc': 4.0.6 + '@tailwindcss/oxide-win32-x64-msvc': 4.0.6 '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)))': dependencies: @@ -7606,12 +7939,20 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) - '@tailwindcss/vite@4.0.5(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + '@tailwindcss/vite@4.0.6(vite@6.1.0(@types/node@18.16.3)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': + dependencies: + '@tailwindcss/node': 4.0.6 + '@tailwindcss/oxide': 4.0.6 + lightningcss: 1.29.1 + tailwindcss: 4.0.6 + vite: 6.1.0(@types/node@18.16.3)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) + + '@tailwindcss/vite@4.0.6(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: - '@tailwindcss/node': 4.0.5 - '@tailwindcss/oxide': 4.0.5 + '@tailwindcss/node': 4.0.6 + '@tailwindcss/oxide': 4.0.6 lightningcss: 1.29.1 - tailwindcss: 4.0.5 + tailwindcss: 4.0.6 vite: 6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0) '@tsconfig/node10@1.0.11': {} @@ -7701,10 +8042,18 @@ snapshots: '@types/node@16.18.126': {} + '@types/node@18.16.3': {} + '@types/node@22.13.1': dependencies: undici-types: 6.20.0 + '@types/pg@8.6.6': + dependencies: + '@types/node': 22.13.1 + pg-protocol: 1.7.1 + pg-types: 2.2.0 + '@types/react-dom@19.0.3(@types/react@19.0.8)': dependencies: '@types/react': 19.0.8 @@ -7717,15 +8066,15 @@ snapshots: '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.19.0 - '@typescript-eslint/type-utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.19.0 - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -7734,14 +8083,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.19.0 '@typescript-eslint/types': 8.19.0 '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.19.0 debug: 4.4.0 - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -7751,12 +8100,12 @@ snapshots: '@typescript-eslint/types': 8.19.0 '@typescript-eslint/visitor-keys': 8.19.0 - '@typescript-eslint/type-utils@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) ts-api-utils: 1.4.3(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: @@ -7772,19 +8121,19 @@ snapshots: fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.0 + semver: 7.7.1 ts-api-utils: 1.4.3(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3)': + '@typescript-eslint/utils@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.19.0 '@typescript-eslint/types': 8.19.0 '@typescript-eslint/typescript-estree': 8.19.0(typescript@5.7.3) - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -7848,6 +8197,13 @@ snapshots: '@vanilla-extract/private@1.0.6': {} + '@vercel/postgres@0.4.1': + dependencies: + '@neondatabase/serverless': 0.5.6 + bufferutil: 4.0.7 + utf-8-validate: 6.0.3 + ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3) + '@vitejs/plugin-react@4.3.4(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0))': dependencies: '@babel/core': 7.26.7 @@ -7917,6 +8273,8 @@ snapshots: dependencies: acorn: 8.14.0 + acorn-walk@8.3.2: {} + acorn-walk@8.3.4: dependencies: acorn: 8.14.0 @@ -7928,12 +8286,12 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@4.1.17(react@19.0.0)(zod@3.24.1): + ai@4.1.34(react@19.0.0)(zod@3.24.1): dependencies: '@ai-sdk/provider': 1.0.7 '@ai-sdk/provider-utils': 2.1.6(zod@3.24.1) - '@ai-sdk/react': 1.1.8(react@19.0.0)(zod@3.24.1) - '@ai-sdk/ui-utils': 1.1.8(zod@3.24.1) + '@ai-sdk/react': 1.1.11(react@19.0.0)(zod@3.24.1) + '@ai-sdk/ui-utils': 1.1.11(zod@3.24.1) '@opentelemetry/api': 1.9.0 jsondiffpatch: 0.6.0 optionalDependencies: @@ -8058,14 +8416,14 @@ snapshots: astring@1.9.0: {} - autoprefixer@10.4.20(postcss@8.5.1): + autoprefixer@10.4.20(postcss@8.5.2): dependencies: browserslist: 4.24.4 caniuse-lite: 1.0.30001696 fraction.js: 4.3.7 normalize-range: 0.1.2 picocolors: 1.1.1 - postcss: 8.5.1 + postcss: 8.5.2 postcss-value-parser: 4.2.0 available-typed-arrays@1.0.7: @@ -8147,6 +8505,15 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bufferutil@4.0.7: + dependencies: + node-gyp-build: 4.8.4 + + bufferutil@4.0.9: + dependencies: + node-gyp-build: 4.8.4 + optional: true + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -8286,6 +8653,18 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + comma-separated-tokens@2.0.3: {} command-line-args@5.2.1: @@ -8323,12 +8702,12 @@ snapshots: cookie-signature@1.2.2: {} + cookie@0.5.0: {} + cookie@0.6.0: {} cookie@0.7.1: {} - cookie@0.7.2: {} - copy-anything@3.0.5: dependencies: is-what: 4.1.16 @@ -8442,6 +8821,9 @@ snapshots: detect-libc@1.0.3: {} + detect-libc@2.0.3: + optional: true + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -8470,6 +8852,15 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv-cli@8.0.0: + dependencies: + cross-spawn: 7.0.6 + dotenv: 16.4.7 + dotenv-expand: 10.0.0 + minimist: 1.2.8 + + dotenv-expand@10.0.0: {} + dotenv@16.4.7: {} dunder-proto@1.0.1: @@ -8508,6 +8899,11 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.2.1 + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 @@ -8777,10 +9173,10 @@ snapshots: escape-string-regexp@5.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.19.0(jiti@2.4.2)): + eslint-compat-utils@0.5.1(eslint@9.20.0(jiti@2.4.2)): dependencies: - eslint: 9.19.0(jiti@2.4.2) - semver: 7.7.0 + eslint: 9.20.0(jiti@2.4.2) + semver: 7.7.1 eslint-import-resolver-node@0.3.9: dependencies: @@ -8790,67 +9186,67 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2)): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.0(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 enhanced-resolve: 5.18.0 - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) fast-glob: 3.3.2 get-tsconfig: 4.8.1 is-bun-module: 1.3.0 is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + eslint-plugin-import-x: 4.6.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@9.19.0(jiti@2.4.2)): + eslint-plugin-es-x@7.8.0(eslint@9.20.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 - eslint: 9.19.0(jiti@2.4.2) - eslint-compat-utils: 0.5.1(eslint@9.19.0(jiti@2.4.2)) + eslint: 9.20.0(jiti@2.4.2) + eslint-compat-utils: 0.5.1(eslint@9.20.0(jiti@2.4.2)) - eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3): + eslint-plugin-import-x@4.6.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3): dependencies: '@types/doctrine': 0.0.9 '@typescript-eslint/scope-manager': 8.19.0 - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 doctrine: 3.0.0 enhanced-resolve: 5.18.0 - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 get-tsconfig: 4.8.1 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.1 stable-hash: 0.0.4 tslib: 2.8.1 transitivePeerDependencies: - supports-color - typescript - eslint-plugin-n@17.15.1(eslint@9.19.0(jiti@2.4.2)): + eslint-plugin-n@17.15.1(eslint@9.20.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0(jiti@2.4.2)) enhanced-resolve: 5.18.0 - eslint: 9.19.0(jiti@2.4.2) - eslint-plugin-es-x: 7.8.0(eslint@9.19.0(jiti@2.4.2)) + eslint: 9.20.0(jiti@2.4.2) + eslint-plugin-es-x: 7.8.0(eslint@9.20.0(jiti@2.4.2)) get-tsconfig: 4.8.1 globals: 15.14.0 ignore: 5.3.2 minimatch: 9.0.5 - semver: 7.6.3 + semver: 7.7.1 - eslint-plugin-promise@7.2.1(eslint@9.19.0(jiti@2.4.2)): + eslint-plugin-promise@7.2.1(eslint@9.20.0(jiti@2.4.2)): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) - eslint: 9.19.0(jiti@2.4.2) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0(jiti@2.4.2)) + eslint: 9.20.0(jiti@2.4.2) - eslint-plugin-react@7.37.3(eslint@9.19.0(jiti@2.4.2)): + eslint-plugin-react@7.37.3(eslint@9.20.0(jiti@2.4.2)): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -8858,7 +9254,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.19.0(jiti@2.4.2) + eslint: 9.20.0(jiti@2.4.2) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -8881,14 +9277,14 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.19.0(jiti@2.4.2): + eslint@9.20.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.19.0(jiti@2.4.2)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.1 - '@eslint/core': 0.10.0 + '@eslint/core': 0.11.0 '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.19.0 + '@eslint/js': 9.20.0 '@eslint/plugin-kit': 0.2.5 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -9499,9 +9895,9 @@ snapshots: dependencies: safer-buffer: 2.1.2 - icss-utils@5.1.0(postcss@8.5.1): + icss-utils@5.1.0(postcss@8.5.2): dependencies: - postcss: 8.5.1 + postcss: 8.5.2 ieee754@1.2.1: {} @@ -9553,6 +9949,9 @@ snapshots: call-bound: 1.0.3 get-intrinsic: 1.2.7 + is-arrayish@0.3.2: + optional: true + is-async-function@2.1.0: dependencies: call-bound: 1.0.3 @@ -9577,7 +9976,7 @@ snapshots: is-bun-module@1.3.0: dependencies: - semver: 7.7.0 + semver: 7.7.1 is-callable@1.2.7: {} @@ -9927,7 +10326,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.0 + semver: 7.7.1 make-error@1.3.6: {} @@ -10615,19 +11014,19 @@ snapshots: mimic-fn@2.1.0: {} - miniflare@3.20250129.0: + miniflare@3.20250204.0(bufferutil@4.0.9): dependencies: '@cspotcode/source-map-support': 0.8.1 acorn: 8.14.0 - acorn-walk: 8.3.4 + acorn-walk: 8.3.2 exit-hook: 2.2.1 glob-to-regexp: 0.4.1 stoppable: 1.1.0 undici: 5.28.5 - workerd: 1.20250129.0 - ws: 8.18.0 - youch: 3.3.4 - zod: 3.24.1 + workerd: 1.20250204.0 + ws: 8.18.0(bufferutil@4.0.9) + youch: 3.2.3 + zod: 3.22.3 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -10706,20 +11105,20 @@ snapshots: negotiator@0.6.3: {} - neostandard@0.12.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3): + neostandard@0.12.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3): dependencies: '@humanwhocodes/gitignore-to-minimatch': 1.0.2 - '@stylistic/eslint-plugin': 2.11.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - eslint: 9.19.0(jiti@2.4.2) - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2)) - eslint-plugin-import-x: 4.6.1(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - eslint-plugin-n: 17.15.1(eslint@9.19.0(jiti@2.4.2)) - eslint-plugin-promise: 7.2.1(eslint@9.19.0(jiti@2.4.2)) - eslint-plugin-react: 7.37.3(eslint@9.19.0(jiti@2.4.2)) + '@stylistic/eslint-plugin': 2.11.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.20.0(jiti@2.4.2) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import-x@4.6.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.0(jiti@2.4.2)) + eslint-plugin-import-x: 4.6.1(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + eslint-plugin-n: 17.15.1(eslint@9.20.0(jiti@2.4.2)) + eslint-plugin-promise: 7.2.1(eslint@9.20.0(jiti@2.4.2)) + eslint-plugin-react: 7.37.3(eslint@9.20.0(jiti@2.4.2)) find-up: 5.0.0 globals: 15.14.0 peowly: 1.3.2 - typescript-eslint: 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) + typescript-eslint: 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) transitivePeerDependencies: - eslint-plugin-import - supports-color @@ -10737,13 +11136,15 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-gyp-build@4.8.4: {} + node-releases@2.0.19: {} normalize-package-data@5.0.0: dependencies: hosted-git-info: 6.1.3 is-core-module: 2.16.1 - semver: 7.7.0 + semver: 7.7.1 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -10752,7 +11153,7 @@ snapshots: npm-install-checks@6.3.0: dependencies: - semver: 7.7.0 + semver: 7.7.1 npm-normalize-package-bin@3.0.1: {} @@ -10760,7 +11161,7 @@ snapshots: dependencies: hosted-git-info: 6.1.3 proc-log: 3.0.0 - semver: 7.7.0 + semver: 7.7.1 validate-npm-package-name: 5.0.1 npm-pick-manifest@8.0.2: @@ -10768,7 +11169,7 @@ snapshots: npm-install-checks: 6.3.0 npm-normalize-package-bin: 3.0.1 npm-package-arg: 10.1.0 - semver: 7.7.0 + semver: 7.7.1 npm-run-path@4.0.1: dependencies: @@ -10976,6 +11377,18 @@ snapshots: is-reference: 3.0.3 zimmerframe: 1.1.2 + pg-int8@1.0.1: {} + + pg-protocol@1.7.1: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -11008,66 +11421,66 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-discard-duplicates@5.1.0(postcss@8.5.1): + postcss-discard-duplicates@5.1.0(postcss@8.5.2): dependencies: - postcss: 8.5.1 + postcss: 8.5.2 - postcss-import@15.1.0(postcss@8.5.1): + postcss-import@15.1.0(postcss@8.5.2): dependencies: - postcss: 8.5.1 + postcss: 8.5.2 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.10 - postcss-js@4.0.1(postcss@8.5.1): + postcss-js@4.0.1(postcss@8.5.2): dependencies: camelcase-css: 2.0.1 - postcss: 8.5.1 + postcss: 8.5.2 - postcss-load-config@4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)): + postcss-load-config@4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)): dependencies: lilconfig: 3.1.3 yaml: 2.7.0 optionalDependencies: - postcss: 8.5.1 + postcss: 8.5.2 ts-node: 10.9.2(@types/node@22.13.1)(typescript@5.7.3) - postcss-modules-extract-imports@3.1.0(postcss@8.5.1): + postcss-modules-extract-imports@3.1.0(postcss@8.5.2): dependencies: - postcss: 8.5.1 + postcss: 8.5.2 - postcss-modules-local-by-default@4.2.0(postcss@8.5.1): + postcss-modules-local-by-default@4.2.0(postcss@8.5.2): dependencies: - icss-utils: 5.1.0(postcss@8.5.1) - postcss: 8.5.1 + icss-utils: 5.1.0(postcss@8.5.2) + postcss: 8.5.2 postcss-selector-parser: 7.0.0 postcss-value-parser: 4.2.0 - postcss-modules-scope@3.2.1(postcss@8.5.1): + postcss-modules-scope@3.2.1(postcss@8.5.2): dependencies: - postcss: 8.5.1 + postcss: 8.5.2 postcss-selector-parser: 7.0.0 - postcss-modules-values@4.0.0(postcss@8.5.1): + postcss-modules-values@4.0.0(postcss@8.5.2): dependencies: - icss-utils: 5.1.0(postcss@8.5.1) - postcss: 8.5.1 + icss-utils: 5.1.0(postcss@8.5.2) + postcss: 8.5.2 - postcss-modules@6.0.1(postcss@8.5.1): + postcss-modules@6.0.1(postcss@8.5.2): dependencies: generic-names: 4.0.0 - icss-utils: 5.1.0(postcss@8.5.1) + icss-utils: 5.1.0(postcss@8.5.2) lodash.camelcase: 4.3.0 - postcss: 8.5.1 - postcss-modules-extract-imports: 3.1.0(postcss@8.5.1) - postcss-modules-local-by-default: 4.2.0(postcss@8.5.1) - postcss-modules-scope: 3.2.1(postcss@8.5.1) - postcss-modules-values: 4.0.0(postcss@8.5.1) + postcss: 8.5.2 + postcss-modules-extract-imports: 3.1.0(postcss@8.5.2) + postcss-modules-local-by-default: 4.2.0(postcss@8.5.2) + postcss-modules-scope: 3.2.1(postcss@8.5.2) + postcss-modules-values: 4.0.0(postcss@8.5.2) string-hash: 1.1.3 - postcss-nested@6.2.0(postcss@8.5.1): + postcss-nested@6.2.0(postcss@8.5.2): dependencies: - postcss: 8.5.1 + postcss: 8.5.2 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.0.10: @@ -11093,11 +11506,27 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.2: + dependencies: + nanoid: 3.3.8 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} prettier@2.8.8: {} - prettier@3.4.2: {} + prettier@3.5.0: {} pretty-ms@7.0.1: dependencies: @@ -11605,7 +12034,7 @@ snapshots: semver@7.6.3: {} - semver@7.7.0: {} + semver@7.7.1: {} send@0.19.0: dependencies: @@ -11660,6 +12089,33 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + optional: true + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -11700,6 +12156,11 @@ snapshots: signal-exit@4.1.0: {} + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + sirv@3.0.0: dependencies: '@polka/url': 1.0.0-next.28 @@ -11742,10 +12203,10 @@ snapshots: spdx-license-ids@3.0.21: {} - spiceflow@1.6.1(@modelcontextprotocol/sdk@1.0.4): + spiceflow@1.6.1(@modelcontextprotocol/sdk@1.4.1): dependencies: '@medley/router': 0.2.1 - '@sinclair/typebox': 0.34.15 + '@sinclair/typebox': 0.34.16 ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) eventsource-parser: 3.0.0 @@ -11754,7 +12215,7 @@ snapshots: zod: 3.24.1 zod-to-json-schema: 3.24.1(zod@3.24.1) optionalDependencies: - '@modelcontextprotocol/sdk': 1.0.4 + '@modelcontextprotocol/sdk': 1.4.1 sprintf-js@1.0.3: {} @@ -11910,7 +12371,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.3.0(react@19.0.0): + swr@2.3.2(react@19.0.0): dependencies: dequal: 2.0.3 react: 19.0.0 @@ -11937,18 +12398,18 @@ snapshots: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.1.1 - postcss: 8.5.1 - postcss-import: 15.1.0(postcss@8.5.1) - postcss-js: 4.0.1(postcss@8.5.1) - postcss-load-config: 4.0.2(postcss@8.5.1)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) - postcss-nested: 6.2.0(postcss@8.5.1) + postcss: 8.5.2 + postcss-import: 15.1.0(postcss@8.5.2) + postcss-js: 4.0.1(postcss@8.5.2) + postcss-load-config: 4.0.2(postcss@8.5.2)(ts-node@10.9.2(@types/node@22.13.1)(typescript@5.7.3)) + postcss-nested: 6.2.0(postcss@8.5.2) postcss-selector-parser: 6.1.2 resolve: 1.22.10 sucrase: 3.35.0 transitivePeerDependencies: - ts-node - tailwindcss@4.0.5: {} + tailwindcss@4.0.6: {} tapable@2.2.1: {} @@ -12145,12 +12606,12 @@ snapshots: possible-typed-array-names: 1.0.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3): + typescript-eslint@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - '@typescript-eslint/parser': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.0(eslint@9.19.0(jiti@2.4.2))(typescript@5.7.3) - eslint: 9.19.0(jiti@2.4.2) + '@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.0(eslint@9.20.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.20.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -12330,6 +12791,10 @@ snapshots: dependencies: react: 19.0.0 + utf-8-validate@6.0.3: + dependencies: + node-gyp-build: 4.8.4 + util-deprecate@1.0.2: {} util@0.12.5: @@ -12456,7 +12921,7 @@ snapshots: vite@5.4.14(@types/node@22.13.1)(lightningcss@1.29.1)(terser@5.31.6): dependencies: esbuild: 0.21.5 - postcss: 8.5.1 + postcss: 8.5.2 rollup: 4.34.0 optionalDependencies: '@types/node': 22.13.1 @@ -12464,6 +12929,20 @@ snapshots: lightningcss: 1.29.1 terser: 5.31.6 + vite@6.1.0(@types/node@18.16.3)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + esbuild: 0.24.2 + postcss: 8.5.1 + rollup: 4.34.6 + optionalDependencies: + '@types/node': 18.16.3 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.29.1 + terser: 5.31.6 + tsx: 4.19.2 + yaml: 2.7.0 + vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.31.6)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 @@ -12599,28 +13078,29 @@ snapshots: wordwrapjs@5.1.0: {} - workerd@1.20250129.0: + workerd@1.20250204.0: optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20250129.0 - '@cloudflare/workerd-darwin-arm64': 1.20250129.0 - '@cloudflare/workerd-linux-64': 1.20250129.0 - '@cloudflare/workerd-linux-arm64': 1.20250129.0 - '@cloudflare/workerd-windows-64': 1.20250129.0 + '@cloudflare/workerd-darwin-64': 1.20250204.0 + '@cloudflare/workerd-darwin-arm64': 1.20250204.0 + '@cloudflare/workerd-linux-64': 1.20250204.0 + '@cloudflare/workerd-linux-arm64': 1.20250204.0 + '@cloudflare/workerd-windows-64': 1.20250204.0 - wrangler@3.107.2(@cloudflare/workers-types@4.20250129.0): + wrangler@3.108.0(@cloudflare/workers-types@4.20250204.0)(bufferutil@4.0.9): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) blake3-wasm: 2.1.5 esbuild: 0.17.19 - miniflare: 3.20250129.0 + miniflare: 3.20250204.0(bufferutil@4.0.9) path-to-regexp: 6.3.0 unenv: 2.0.0-rc.1 - workerd: 1.20250129.0 + workerd: 1.20250204.0 optionalDependencies: - '@cloudflare/workers-types': 4.20250129.0 + '@cloudflare/workers-types': 4.20250204.0 fsevents: 2.3.3 + sharp: 0.33.5 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -12639,9 +13119,18 @@ snapshots: wrappy@1.0.2: {} - ws@7.5.10: {} + ws@7.5.10(bufferutil@4.0.9): + optionalDependencies: + bufferutil: 4.0.9 - ws@8.18.0: {} + ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3): + optionalDependencies: + bufferutil: 4.0.7 + utf-8-validate: 6.0.3 + + ws@8.18.0(bufferutil@4.0.9): + optionalDependencies: + bufferutil: 4.0.9 xtend@4.0.2: {} @@ -12669,9 +13158,9 @@ snapshots: yocto-queue@0.1.0: {} - youch@3.3.4: + youch@3.2.3: dependencies: - cookie: 0.7.2 + cookie: 0.5.0 mustache: 4.2.0 stacktracey: 2.1.8 @@ -12681,6 +13170,8 @@ snapshots: dependencies: zod: 3.24.1 + zod@3.22.3: {} + zod@3.24.1: {} zwitch@2.0.4: {} diff --git a/spiceflow/package.json b/spiceflow/package.json index f1813d2..bd56280 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -57,7 +57,6 @@ "license": "", "dependencies": { "@hiogawa/transforms": "^0.0.0", - "@jacob-ebey/vite-react-server-dom": "^0.0.12", "@sinclair/typebox": "^0.34.14", "@vitejs/plugin-react": "^4.3.4", "ajv": "^8.17.1", diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 394344a..c67e70b 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -138,7 +138,6 @@ export function spiceflowPlugin({ entry }): PluginOption { optimizeDeps: { include: [ 'react-dom/client', - 'react-server-dom-vite/client', 'spiceflow/dist/react/server-dom-client-optimized', ], }, From 981aaa251f60da8dbe3b3a91d2ca50de743c5b97 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 18:03:04 +0100 Subject: [PATCH 051/100] added pokemon view, works well --- how-is-this-not-illegal/app/main.tsx | 37 ++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/how-is-this-not-illegal/app/main.tsx b/how-is-this-not-illegal/app/main.tsx index e3f4c98..b2c3139 100644 --- a/how-is-this-not-illegal/app/main.tsx +++ b/how-is-this-not-illegal/app/main.tsx @@ -2,6 +2,7 @@ import './globals.css' import { Spiceflow } from 'spiceflow' import { sql } from '@vercel/postgres' import { Suspense } from 'react' +import { Link } from 'spiceflow/dist/react/components' const app = new Spiceflow() .layout('/*', async ({ children }) => { @@ -17,11 +18,40 @@ const app = new Spiceflow() return ( {rows.map((p) => ( - + + + ))} ) }) + .layout('/pokemon/:id', ({ children }) => { + return ( +
    + Loading...
    }>{children} + + ) + }) + .page('/pokemon/:id', async function PokemonDetails({ params: { id } }) { + const { rows } = await sql`SELECT * FROM pokemon WHERE id = ${id}` + const pokemon = rows[0] + + if (!pokemon) { + return
    Pokemon not found
    + } + + return ( + + ) + }) function Loading() { return ( @@ -76,10 +106,7 @@ function RootLayout({ children }: { children: React.ReactNode }) { property="og:description" content="Querying Postgres directly from your components" /> - + From 54ffe2612f8f1e5056d7f29c08f481041af644fe Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 20:01:06 +0100 Subject: [PATCH 052/100] added progress component --- example-react/src/app/layout.tsx | 2 + spiceflow/src/react/progress.tsx | 148 +++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 spiceflow/src/react/progress.tsx diff --git a/example-react/src/app/layout.tsx b/example-react/src/app/layout.tsx index e6d9744..cb6c11b 100644 --- a/example-react/src/app/layout.tsx +++ b/example-react/src/app/layout.tsx @@ -1,4 +1,5 @@ import { Link } from "spiceflow/dist/react/components"; +import { ProgressBar } from "spiceflow/dist/react/progress"; export function Layout(props: React.PropsWithChildren) { return ( @@ -11,6 +12,7 @@ export function Layout(props: React.PropsWithChildren) { /> +
    • Home diff --git a/spiceflow/src/react/progress.tsx b/spiceflow/src/react/progress.tsx new file mode 100644 index 0000000..5ea7d65 --- /dev/null +++ b/spiceflow/src/react/progress.tsx @@ -0,0 +1,148 @@ +'use client' +import { useEffect, useRef, useState } from 'react' + +import { ReactNode } from 'react' +import { router } from './router.js' + +export interface ProgressBarProps { + /** + * Color of the progress bar + * @default "#0ea5e9" + */ + color?: string + /** + * Duration of the transition animation in milliseconds + * @default 300 + */ + duration?: number +} + +export function ProgressBar({ + color = '#0ea5e9', + duration = 300, +}: ProgressBarProps) { + const progress = useProgress() + const [isExiting, setIsExiting] = useState(false) + + useEffect(() => { + if (progress.state === 'complete') { + setIsExiting(true) + } else { + setIsExiting(false) + } + }, [progress.state]) + + return ( +
      { + if (e.propertyName === 'opacity' && isExiting) { + progress.reset() + setIsExiting(false) + } + }} + /> + ) +} + +function useProgress() { + const [state, setState] = useState< + 'initial' | 'in-progress' | 'completing' | 'complete' + >('initial') + const [width, setWidth] = useState(0) + + useInterval( + () => { + if (state === 'in-progress') { + setWidth((prev) => { + let diff + if (prev === 0) { + diff = 15 + } else if (prev < 50) { + diff = rand(1, 10) + } else { + diff = rand(1, 5) + } + return Math.min(prev + diff, 99) + }) + } + }, + state === 'in-progress' ? 750 : null, + ) + + useEffect(() => { + const unlisten = router.listen(() => { + start() + }) + + return () => { + unlisten() + } + }, []) + + useEffect(() => { + if (state === 'initial') { + setWidth(0) + } else if (state === 'completing') { + setWidth(100) + } + }, [state]) + + useEffect(() => { + if (width === 100) { + setState('complete') + } + }, [width]) + + function reset() { + setState('initial') + } + + function start() { + setState('in-progress') + } + + function done() { + setState((prev) => + prev === 'initial' || prev === 'in-progress' ? 'completing' : prev, + ) + } + + return { state, width, start, done, reset } +} + +function rand(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min +} + +function useInterval(callback: () => void, delay: number | null) { + const savedCallback = useRef(callback) + + useEffect(() => { + savedCallback.current = callback + }, [callback]) + + useEffect(() => { + function tick() { + savedCallback.current() + } + + if (delay !== null) { + tick() + const id = setInterval(tick, delay) + return () => clearInterval(id) + } + }, [delay]) +} From 594cb6388e05e1483ed6532f07280e0c223c2a3c Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 20:07:50 +0100 Subject: [PATCH 053/100] payload is always a promise, this way navigations state does not trigger suspense boundaries, no need for await --- spiceflow/src/react/components.tsx | 18 ++++++++++--- spiceflow/src/react/entry.client.tsx | 27 ++++++++++---------- spiceflow/src/react/entry.ssr.tsx | 38 +++++++++++++--------------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 808cdb7..f16a825 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -3,18 +3,28 @@ import React, { Suspense } from 'react' import { ReactFormState } from 'react-dom/client' import { router } from './router.js' +import { ServerPayload } from '../spiceflow.js' -export const FlightDataContext = React.createContext(undefined!) +export const FlightDataContext = React.createContext>( + undefined!, +) // Get $$id property that was set by registerClientReference export function useFlightData() { - // return React.use(React.useContext(FlightDataContext)) - return React.useContext(FlightDataContext) + const c = React.useContext(FlightDataContext) + if (c instanceof Promise) { + return React.use(c)?.root + } + return c?.['root'] + // return React.useContext(FlightDataContext) } -export function LayoutContent(props: { id: string }) { +export function LayoutContent(props: { id?: string }) { const data = useFlightData() const elem = (() => { + if (!props.id) { + return data.layouts[0]?.element ?? data.page + } const layoutIndex = data.layouts.findIndex( (layout) => layout.id === props.id, ) diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index a877f33..aba1272 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -10,6 +10,7 @@ import { DefaultGlobalErrorPage, ErrorBoundary, FlightDataContext, + LayoutContent, } from './components.js' import { ServerPayload } from '../spiceflow.js' @@ -17,7 +18,7 @@ async function main() { const callServer: CallServerFn = async (id, args) => { const url = new URL(window.location.href) url.searchParams.set('__rsc', id) - const payload = await ReactClient.createFromFetch( + const payloadPromise = ReactClient.createFromFetch( fetch(url, { method: 'POST', body: await ReactClient.encodeReply(args), @@ -26,20 +27,20 @@ async function main() { { callServer }, ) // console.log({ 'action payload': payload }) - setPayload(payload) + setPayload(payloadPromise) + let payload = await payloadPromise return payload.returnValue } Object.assign(globalThis, { __callServer: callServer }) - const initialPayload = - await ReactClient.createFromReadableStream( - rscStream, - clientReferenceManifest, + const initialPayload = ReactClient.createFromReadableStream( + rscStream, + clientReferenceManifest, - { callServer }, - ) + { callServer }, + ) - let setPayload: (v: ServerPayload) => void + let setPayload: (v: Promise) => void function BrowserRoot() { const [payload, setPayload_] = React.useState(initialPayload) @@ -54,7 +55,7 @@ async function main() { console.log('onNavigation') const url = new URL(window.location.href) url.searchParams.set('__rsc', '') - const payload = await ReactClient.createFromFetch( + const payload = ReactClient.createFromFetch( fetch(url), clientReferenceManifest, @@ -66,15 +67,15 @@ async function main() { return ( - - {payload.root?.layouts?.[0]?.element ?? payload.root.page} + + ) } ReactDomClient.hydrateRoot(document, , { - formState: initialPayload.formState, + formState: (await initialPayload).formState, }) if (import.meta.hot) { diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index a5a2878..5129e0c 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -3,24 +3,19 @@ import ReactDOMServer from 'react-dom/server.edge' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' import type { ModuleRunner } from 'vite/module-runner' -import { - createRequest, - fromPipeableToWebReadable, - fromWebToNodeReadable, - sendResponse, -} from './utils/fetch.js' import { injectRSCPayload } from 'rsc-html-stream/server' +import cssUrls from 'virtual:app-styles' +import { ServerPayload } from '../spiceflow.js' import { - DefaultGlobalErrorPage, - ErrorBoundary, - FlightDataContext, + FlightDataContext, + LayoutContent } from './components.js' -import { bootstrapModules } from 'virtual:ssr-assets' import { clientReferenceManifest } from './utils/client-reference.js' -import cssUrls from 'virtual:app-styles' -import { ServerPayload } from '../spiceflow.js' -import { Suspense } from 'react' -import { sleep } from 'spiceflow/dist/utils' +import { + createRequest, + fromWebToNodeReadable, + sendResponse +} from './utils/fetch.js' export default async function handler( req: IncomingMessage, @@ -43,30 +38,31 @@ export default async function handler( const [flightStream1, flightStream2] = response.body!.tee() - const payload = await ReactClient.createFromNodeStream( + const payloadPromise = ReactClient.createFromNodeStream( fromWebToNodeReadable(flightStream1), clientReferenceManifest, ) const ssrAssets = await import('virtual:ssr-assets') const el = ( - + {cssUrls.map((url) => ( // precedence to force head rendering // https://react.dev/reference/react-dom/components/link#special-rendering-behavior ))} - {payload.root?.layouts?.[0]?.element ?? payload.root.page} + ) let htmlStream: ReadableStream let status = 200 + let payload = await payloadPromise try { htmlStream = await ReactDOMServer.renderToReadableStream(el, { bootstrapModules: ssrAssets.bootstrapModules, formState: payload.formState, - onError(e, ) { + onError(e) { // This also throws outside, no need to do anything here console.error('[react-dom:renderToPipeableStream]', e) if (e instanceof Response) { @@ -80,12 +76,12 @@ export default async function handler( console.log(`error during ssr render catch`, e) // On error, render minimal HTML shell // Client will do full CSR render and show error boundary - + if (e instanceof Response) { sendResponse(e, res) return } - // https://bsky.app/profile/ebey.bsky.social/post/3lev4lqr2ak2j + // https://bsky.app/profile/ebey.bsky.social/post/3lev4lqr2ak2j const errorRoot = ( @@ -117,7 +113,7 @@ export default async function handler( }, }, ) - + console.log(`sending response`) sendResponse(htmlResponse, res) } From 106e81280155f6a554c2587a07e936a8f2fc1f1c Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 22:21:59 +0100 Subject: [PATCH 054/100] fix support for redirect, fix hot module replacement for client imports, fix redirects in client side too --- example-react/e2e/basic.test.ts | 29 ++++++----- example-react/src/app/client.tsx | 3 ++ example-react/src/main.tsx | 40 ++++++++------- spiceflow/play.js | 43 ++++++++++++++++ spiceflow/src/react/components.tsx | 40 +++++++-------- spiceflow/src/react/entry.ssr.tsx | 45 ++++++++++------- spiceflow/src/react/errors.tsx | 69 ++++++++++++++++++++++++++ spiceflow/src/react/types/ambient.d.ts | 8 +-- spiceflow/src/react/utils/fetch.ts | 6 --- spiceflow/src/react/utils/normalize.ts | 3 +- spiceflow/src/spiceflow.tsx | 52 +++++++++++++++---- spiceflow/src/utils.ts | 16 ++---- spiceflow/src/vite.tsx | 32 ++++++------ 13 files changed, 268 insertions(+), 118 deletions(-) create mode 100644 spiceflow/play.js create mode 100644 spiceflow/src/react/errors.tsx diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index eb923e2..c6bac1a 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -1,6 +1,22 @@ import { type Page, expect, test } from "@playwright/test"; import { createEditor } from "./helper.js"; + + +test.describe("redirect", () => { + test("redirect in outer route scope", async ({ page }) => { + await page.goto("/redirect"); + await expect(page).toHaveURL("/"); + await page.getByText("[hydrated: 1]").click(); + }); + test.skip("redirect in RSC", async ({ page }) => { + await page.goto("/redirect-in-rsc"); + await expect(page).toHaveURL("/"); + await page.getByText("[hydrated: 1]").click(); + }); +}); + + test("client reference", async ({ page }) => { await page.goto("/"); await page.getByText("[hydrated: 1]").click(); @@ -19,19 +35,6 @@ test("server reference in server @js", async ({ page }) => { }); -test.describe("redirect", () => { - test("redirect in outer route scope", async ({ page }) => { - await page.goto("/redirect"); - await expect(page).toHaveURL("/"); - await page.getByText("[hydrated: 1]").click(); - }); - test.skip("redirect in RSC", async ({ page }) => { - await page.goto("/redirect-in-rsc"); - await expect(page).toHaveURL("/"); - await page.getByText("[hydrated: 1]").click(); - }); -}); - diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index 8203fce..1c19044 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -72,3 +72,6 @@ export function ClientComponentThrows() { } + + + diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index ca013c2..a162332 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -3,9 +3,10 @@ import { Suspense } from "react"; import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; import "./styles.css"; + import { ClientComponentThrows } from "./app/client"; import { ErrorBoundary } from "spiceflow/dist/react/components"; -import { sleep } from "spiceflow/dist/utils"; +import { redirect, sleep } from "spiceflow/dist/utils"; const app = new Spiceflow() .layout("/*", async ({ children, request }) => { @@ -27,13 +28,24 @@ const app = new Spiceflow() }) .get("/hello", () => "Hello, World!") - .page("/redirect", async () => { - throw new Response("Redirect", { - status: 302, - headers: { - location: "/", - }, - }); + .page("/top-level-redirect", async () => { + throw redirect("/"); + }) + .page("/redirect-in-rsc", async () => { + return ; + }) + .page("/slow-redirect", async ({ request, children }) => { + await sleep(100); + + throw redirect("/"); + }) + + .page("/redirect-in-rsc-suspense", async () => { + return ( + redirecting...
      }> + + + ); }) .layout("/page/*", async ({ request, children }) => { return ( @@ -113,21 +125,15 @@ const app = new Spiceflow() .page("/client-error", async () => { return ; }) - .page("/redirect-in-rsc", async () => { - return ; - }) + .post("/echo", async ({ request }) => { const body = await request.json(); return { echo: body }; }); async function Redirects() { - throw new Response("Redirect", { - status: 302, - headers: { - location: "/", - }, - }); + await sleep(100); + throw redirect("/"); return
      Redirect
      ; } diff --git a/spiceflow/play.js b/spiceflow/play.js new file mode 100644 index 0000000..0456d0f --- /dev/null +++ b/spiceflow/play.js @@ -0,0 +1,43 @@ +class FastError { + constructor(message) {} +} + +function benchmark(name, fn, iterations = 100000) { + // Warmup + for (let i = 0; i < 10000; i++) { + fn() + } + + const start = process.hrtime.bigint() + for (let i = 0; i < iterations; i++) { + fn() + } + const end = process.hrtime.bigint() + console.log(`${name}: ${Number(end - start) / 1_000_000}ms`) +} + +// Normal Error +benchmark('Normal Error', () => { + try { + throw new Error('test') + } catch (e) {} +}) + +// // No Stack Error +// Error.stackTraceLimit = 0 +// benchmark('No Stack Error', () => { +// try { +// throw new Error('test') +// } catch (e) {} +// }) + +// Fast Error +benchmark('Fast Error', () => { + try { + throw new FastError('test') + } catch (e) {} +}) +Object.setPrototypeOf(FastError.prototype, Error.prototype) +Object.setPrototypeOf(FastError, Error) + +console.log(new FastError() instanceof Error) diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index f16a825..191eec8 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -4,6 +4,7 @@ import React, { Suspense } from 'react' import { ReactFormState } from 'react-dom/client' import { router } from './router.js' import { ServerPayload } from '../spiceflow.js' +import { isRedirectError, isNotFoundError, getErrorContext } from './errors.js' export const FlightDataContext = React.createContext>( undefined!, @@ -12,23 +13,28 @@ export const FlightDataContext = React.createContext>( export function useFlightData() { const c = React.useContext(FlightDataContext) - if (c instanceof Promise) { - return React.use(c)?.root + + const payload = React.use(c) + let root = payload?.root + if (!root) { + console.log('root not found', payload) } - return c?.['root'] + return root + // return React.useContext(FlightDataContext) } export function LayoutContent(props: { id?: string }) { const data = useFlightData() + if (!data) return null const elem = (() => { if (!props.id) { - return data.layouts[0]?.element ?? data.page + return data?.layouts[0]?.element ?? data.page } - const layoutIndex = data.layouts.findIndex( + const layoutIndex = data?.layouts.findIndex( (layout) => layout.id === props.id, ) - let nextLayout = data.layouts[layoutIndex + 1]?.element + let nextLayout = data?.layouts[layoutIndex + 1]?.element if (nextLayout) { return nextLayout } @@ -54,9 +60,9 @@ export type ActionResult = { data?: ReactFormState | null } -// TODO not implemented interface ReactServerErrorContext { status: number + location?: string headers?: Record } @@ -75,18 +81,6 @@ interface State { error: Error | null } -function isRedirectError(ctx: ReactServerErrorContext) { - return ctx.status >= 300 && ctx.status < 400 -} - -function isNotFoundError(ctx: ReactServerErrorContext) { - return ctx.status === 404 -} - -function getErrorContext(error: Error): ReactServerErrorContext | undefined { - return (error as any).serverError -} - export function ErrorBoundary(props: Props) { return } @@ -99,8 +93,12 @@ class ErrorBoundary_ extends React.Component { static getDerivedStateFromError(error: Error) { const ctx = getErrorContext(error) - if (ctx && (isNotFoundError(ctx) || isRedirectError(ctx))) { - throw error + if (ctx && isRedirectError(ctx) && ctx.headers?.['location']) { + console.log('redirecting from browser to', ctx.headers?.['location']) + router.replace(ctx.headers?.['location']) + } + if (ctx && isNotFoundError(ctx)) { + // TODO somehow show the not found page } return { error } } diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 5129e0c..5077e20 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -6,16 +6,14 @@ import type { ModuleRunner } from 'vite/module-runner' import { injectRSCPayload } from 'rsc-html-stream/server' import cssUrls from 'virtual:app-styles' import { ServerPayload } from '../spiceflow.js' -import { - FlightDataContext, - LayoutContent -} from './components.js' +import { FlightDataContext, LayoutContent } from './components.js' import { clientReferenceManifest } from './utils/client-reference.js' import { - createRequest, - fromWebToNodeReadable, - sendResponse + createRequest, + fromWebToNodeReadable, + sendResponse, } from './utils/fetch.js' +import { getErrorContext, isNotFoundError, isRedirectError } from './errors.js' export default async function handler( req: IncomingMessage, @@ -57,28 +55,37 @@ export default async function handler( let htmlStream: ReadableStream let status = 200 - let payload = await payloadPromise try { + let payload = await payloadPromise htmlStream = await ReactDOMServer.renderToReadableStream(el, { bootstrapModules: ssrAssets.bootstrapModules, formState: payload.formState, onError(e) { // This also throws outside, no need to do anything here - console.error('[react-dom:renderToPipeableStream]', e) - if (e instanceof Response) { - console.log('sending response') - sendResponse(e, res) - return - } + console.error('[entry.srr.tsx:renderToPipeableStream]', e) + return e?.digest || e?.message }, }) } catch (e) { console.log(`error during ssr render catch`, e) - // On error, render minimal HTML shell - // Client will do full CSR render and show error boundary - - if (e instanceof Response) { - sendResponse(e, res) + let errCtx = getErrorContext(e) + if (errCtx && isRedirectError(errCtx)) { + console.log(`redirecting to ${errCtx.headers?.location}`) + sendResponse( + new Response(errCtx.headers?.location, { + status: errCtx.status, + headers: errCtx.headers, + }), + res, + ) + return + } + if (errCtx && isNotFoundError(errCtx)) { + // TODO show a not found component instead + sendResponse( + new Response('404', { status: errCtx.status, headers: errCtx.headers }), + res, + ) return } // https://bsky.app/profile/ebey.bsky.social/post/3lev4lqr2ak2j diff --git a/spiceflow/src/react/errors.tsx b/spiceflow/src/react/errors.tsx new file mode 100644 index 0000000..9561a8a --- /dev/null +++ b/spiceflow/src/react/errors.tsx @@ -0,0 +1,69 @@ +export interface ReactServerErrorContext { + status: number + headers?: Record +} + +export class ReactServerDigestError extends Error { + constructor(public digest: string) { + super('ReactServerError') + } +} +// TODO make redirects faster with this +// Object.setPrototypeOf(FastError.prototype, Error.prototype) +// Object.setPrototypeOf(FastError, Error) + +export function createError(ctx: ReactServerErrorContext) { + const digest = `__REACT_SERVER_ERROR__:${JSON.stringify(ctx)}` + return new ReactServerDigestError(digest) +} + +export function redirect( + location: string, + options?: { status?: number; headers?: Record }, +) { + return createError({ + status: options?.status ?? 307, + headers: { + ...options?.headers, + location, + }, + }) +} + +export function isRedirectError(ctx?: ReactServerErrorContext) { + if (!ctx) return false + const location = ctx.headers?.['location'] + if (300 <= ctx.status && ctx.status <= 399 && typeof location === 'string') { + return { location } + } + return false +} + +export function isRedirectStatus(status: number) { + return 300 <= status && status <= 399 +} + +export function isNotFoundError(ctx?: ReactServerErrorContext) { + if (!ctx) return false + return ctx.status === 404 +} + +export function getErrorContext( + error: unknown, +): ReactServerErrorContext | undefined { + if ( + error instanceof Error && + 'digest' in error && + typeof error.digest === 'string' + ) { + const m = error.digest.match(/^__REACT_SERVER_ERROR__:(.*)$/) + if (m && m[1]) { + try { + return JSON.parse(m[1]) + } catch (e) { + console.error(e) + } + } + } + return +} diff --git a/spiceflow/src/react/types/ambient.d.ts b/spiceflow/src/react/types/ambient.d.ts index e8f2fed..003a6c3 100644 --- a/spiceflow/src/react/types/ambient.d.ts +++ b/spiceflow/src/react/types/ambient.d.ts @@ -30,10 +30,10 @@ declare module 'react-server-dom-vite/server' { const defaultExport: { registerServerReference: Function registerClientReference: Function - decodeReply: decodeReply - decodeAction: decodeAction - decodeFormState: decodeFormState - renderToPipeableStream: renderToPipeableStream + decodeReply: typeof decodeReply + decodeAction: typeof decodeAction + decodeFormState: typeof decodeFormState + renderToPipeableStream: typeof renderToPipeableStream } export default defaultExport } diff --git a/spiceflow/src/react/utils/fetch.ts b/spiceflow/src/react/utils/fetch.ts index 299a5f3..87ada85 100644 --- a/spiceflow/src/react/utils/fetch.ts +++ b/spiceflow/src/react/utils/fetch.ts @@ -66,12 +66,6 @@ export function sendResponse(response: Response, res: ServerResponse) { } } -export function fromPipeableToWebReadable(stream: PipeableStream) { - return Readable.toWeb( - stream.pipe(new PassThrough()), - ) as ReadableStream -} - export function fromWebToNodeReadable(stream: ReadableStream) { return Readable.fromWeb(stream as any) } diff --git a/spiceflow/src/react/utils/normalize.ts b/spiceflow/src/react/utils/normalize.ts index c5e5ac0..25b52e8 100644 --- a/spiceflow/src/react/utils/normalize.ts +++ b/spiceflow/src/react/utils/normalize.ts @@ -8,7 +8,7 @@ import { ModuleNode, ViteDevServer } from 'vite' export function noramlizeClientReferenceId( id: string, parentServer: ViteDevServer, - mod?: ModuleNode, + mod?: { lastHMRTimestamp: number }, ) { const root = parentServer.config.root if (id.startsWith(root)) { @@ -25,6 +25,7 @@ export function noramlizeClientReferenceId( if (mod && mod.lastHMRTimestamp > 0) { id += `?t=${mod.lastHMRTimestamp}` } + return id } diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index 20f978e..a5bcdaf 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -37,20 +37,16 @@ import { MiddlewareContext } from './context.js' import { isProduction, ValidationError } from './error.js' import { isAsyncIterable, isResponse, redirect } from './utils.js' -import { - DefaultGlobalErrorPage, - ErrorBoundary, - FlightData, - LayoutContent, -} from './react/components.js' +import { PassThrough, Readable } from 'stream' +import { FlightData, LayoutContent } from './react/components.js' import { ClientReferenceMetadataManifest, ServerReferenceManifest, } from './react/types/index.js' -import { fromPipeableToWebReadable } from './react/utils/fetch.js' import { TrieRouter } from './trie-router/router.js' import { decodeURIComponent_ } from './trie-router/url.js' import { Result } from './trie-router/utils.js' +import { isRedirectError } from './react/errors.js' const ajv = (addFormats.default || addFormats)( new (Ajv.default || Ajv)({ useDefaults: true }), @@ -888,6 +884,7 @@ export class Spiceflow< ( { root, @@ -963,13 +962,33 @@ export class Spiceflow< formState, }, clientReferenceMetadataManifest, - { onError(error) {} }, + { + onError(error) { + console.error('[spiceflow:renderToPipeableStream]', error) + thrownError = error + return error?.digest || error?.message + }, + }, ) - // render flight stream - const stream = fromPipeableToWebReadable(abortable) request.signal.addEventListener('abort', () => { abortable.abort() }) + const passthrough = new PassThrough() + const nodeStream = abortable.pipe(passthrough) + const stream = Readable.toWeb(nodeStream) as ReadableStream + + const timerId = `wait for first chunk ${Math.random().toString(36).slice(2)}` + console.time(timerId) + await new Promise((resolve) => { + passthrough.once('data', () => { + resolve() + }) + }) + console.timeEnd(timerId) + + if (thrownError instanceof Response) { + return thrownError + } return new Response(stream, { headers: { @@ -1020,12 +1039,19 @@ export class Spiceflow< let handlerResponse: Response | undefined async function getResForError(err: any) { + if (isRedirectError(err)) { + return new Response(err.location, { + status: err.status, + headers: err.headers, + }) + } if (isResponse(err)) return err let res = await self.runErrorHandlers({ onErrorHandlers, error: err, request, }) + if (isResponse(res)) return res let status = err?.status ?? 500 @@ -1108,6 +1134,12 @@ export class Spiceflow< if (isResponse(res)) { return res } + if (isRedirectError(err)) { + return new Response(err.location, { + status: err.status, + headers: err.headers, + }) + } } } } diff --git a/spiceflow/src/utils.ts b/spiceflow/src/utils.ts index 0bfcfb9..0677b1f 100644 --- a/spiceflow/src/utils.ts +++ b/spiceflow/src/utils.ts @@ -1,3 +1,5 @@ +import { redirect } from './react/errors.js' + // deno-lint-ignore no-explicit-any export const deepFreeze = (value: any) => { for (const key of Reflect.ownKeys(value)) { @@ -26,6 +28,8 @@ export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } +export { redirect } + export const StatusMap = { Continue: 100, 'Switching Protocols': 101, @@ -98,18 +102,6 @@ export const InvertedStatusMap = Object.fromEntries( export type StatusMap = typeof StatusMap export type InvertedStatusMap = typeof InvertedStatusMap -/** - * - * @param url URL to redirect to - * @param HTTP status code to send, - */ -export const redirect = ( - url: string, - status: 301 | 302 | 303 | 307 | 308 = 302, -) => Response.redirect(url, status) - -export type redirect = typeof redirect - export function isResponse(result: any): result is Response { if (result instanceof Response) { return true diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index c67e70b..3507068 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -13,7 +13,7 @@ import { PluginOption, type RunnableDevEnvironment, ViteDevServer, - createRunnableDevEnvironment + createRunnableDevEnvironment, } from 'vite' import { collectStyleUrls } from './react/css.js' import { noramlizeClientReferenceId } from './react/utils/normalize.js' @@ -66,22 +66,26 @@ export function spiceflowPlugin({ entry }): PluginOption { // This is needed to let scan discover server references found in the use client components return } - const mod = await server?.moduleGraph?.getModuleByUrl(id) - let generateId = (filename, directive) => { - let id = '' + const mod = + await server?.environments.client.moduleGraph.getModuleById(id) + // console.log('mod', id, mod?.lastHMRTimestamp) + // console.log([...server?.moduleGraph.idToModuleMap.keys()]) + let generateId = (id) => { + let generated = '' if (command === 'build') { - id = makeHash(filename) + generated = makeHash(id) } else { - id = noramlizeClientReferenceId(filename, server, mod) + generated = noramlizeClientReferenceId(id, server, mod) } - console.log('generateId', id) - if (directive === 'use server') { - serverModules.set(filename, id) - return id + console.log('generateId', generated) + + if (!isUseClient) { + serverModules.set(id, generated) + } else { + clientModules.set(id, generated) } - clientModules.set(filename, id) - return id + return generated } if (this.environment.name === 'rsc') { @@ -128,9 +132,7 @@ export function spiceflowPlugin({ entry }): PluginOption { }, { name: 'spiceflow', - configureServer(_server) { - server = _server - }, + config: () => ({ appType: 'custom', environments: { From 025379587d81ee859ff1bf08351e6cddbd200e96 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 22:25:08 +0100 Subject: [PATCH 055/100] all redirect tests pass --- example-react/e2e/basic.test.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index c6bac1a..38aa93b 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -1,22 +1,29 @@ import { type Page, expect, test } from "@playwright/test"; import { createEditor } from "./helper.js"; - - test.describe("redirect", () => { test("redirect in outer route scope", async ({ page }) => { - await page.goto("/redirect"); + await page.goto("/top-level-redirect"); await expect(page).toHaveURL("/"); await page.getByText("[hydrated: 1]").click(); }); - test.skip("redirect in RSC", async ({ page }) => { + test("redirect in RSC", async ({ page }) => { await page.goto("/redirect-in-rsc"); await expect(page).toHaveURL("/"); await page.getByText("[hydrated: 1]").click(); }); + test("redirect in RSC, slow (meaning not first rsc chunk)", async ({ page }) => { + await page.goto("/slow-redirect"); + await expect(page).toHaveURL("/"); + await page.getByText("[hydrated: 1]").click(); + }); + test("redirect in RSC inside suspense, redirect made by client", async ({ page }) => { + await page.goto("/redirect-in-rsc-suspense"); + await expect(page).toHaveURL("/"); + await page.getByText("[hydrated: 1]").click(); + }); }); - test("client reference", async ({ page }) => { await page.goto("/"); await page.getByText("[hydrated: 1]").click(); @@ -34,10 +41,6 @@ test("server reference in server @js", async ({ page }) => { await testServerAction(page); }); - - - - test.describe(() => { test.use({ javaScriptEnabled: false }); test("server reference in server @nojs", async ({ page }) => { From 8331a4167b2b91b195a8532af54d9bc83dba14a6 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 22:42:17 +0100 Subject: [PATCH 056/100] separating hooks from components --- spiceflow/src/react/components.tsx | 18 +----------------- spiceflow/src/react/context.tsx | 21 +++++++++++++++++++++ spiceflow/src/react/entry.client.tsx | 3 ++- spiceflow/src/react/entry.ssr.tsx | 3 ++- spiceflow/src/react/router.tsx | 16 ++++++++++++++-- spiceflow/src/vite.tsx | 2 +- 6 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 spiceflow/src/react/context.tsx diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 191eec8..ac573b4 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -5,24 +5,8 @@ import { ReactFormState } from 'react-dom/client' import { router } from './router.js' import { ServerPayload } from '../spiceflow.js' import { isRedirectError, isNotFoundError, getErrorContext } from './errors.js' +import { useFlightData } from './context.js' -export const FlightDataContext = React.createContext>( - undefined!, -) -// Get $$id property that was set by registerClientReference - -export function useFlightData() { - const c = React.useContext(FlightDataContext) - - const payload = React.use(c) - let root = payload?.root - if (!root) { - console.log('root not found', payload) - } - return root - - // return React.useContext(FlightDataContext) -} export function LayoutContent(props: { id?: string }) { const data = useFlightData() diff --git a/spiceflow/src/react/context.tsx b/spiceflow/src/react/context.tsx new file mode 100644 index 0000000..cda581c --- /dev/null +++ b/spiceflow/src/react/context.tsx @@ -0,0 +1,21 @@ +import React from "react" +import { ServerPayload } from "../spiceflow.js" + +export const FlightDataContext = React.createContext>( + undefined!, +) + +// Get $$id property that was set by registerClientReference + +export function useFlightData() { + const c = React.useContext(FlightDataContext) + + const payload = React.use(c) + let root = payload?.root + if (!root) { + console.log('root not found', payload) + } + return root + + // return React.useContext(FlightDataContext) +} diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index aba1272..363ce72 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -9,10 +9,11 @@ import { rscStream } from 'rsc-html-stream/client' import { DefaultGlobalErrorPage, ErrorBoundary, - FlightDataContext, + LayoutContent, } from './components.js' import { ServerPayload } from '../spiceflow.js' +import { FlightDataContext } from './context.js' async function main() { const callServer: CallServerFn = async (id, args) => { diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 5077e20..f013c39 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -6,7 +6,7 @@ import type { ModuleRunner } from 'vite/module-runner' import { injectRSCPayload } from 'rsc-html-stream/server' import cssUrls from 'virtual:app-styles' import { ServerPayload } from '../spiceflow.js' -import { FlightDataContext, LayoutContent } from './components.js' +import { LayoutContent } from './components.js' import { clientReferenceManifest } from './utils/client-reference.js' import { createRequest, @@ -14,6 +14,7 @@ import { sendResponse, } from './utils/fetch.js' import { getErrorContext, isNotFoundError, isRedirectError } from './errors.js' +import { FlightDataContext } from './context.js' export default async function handler( req: IncomingMessage, diff --git a/spiceflow/src/react/router.tsx b/spiceflow/src/react/router.tsx index 8bb4234..024f4ae 100644 --- a/spiceflow/src/react/router.tsx +++ b/spiceflow/src/react/router.tsx @@ -1,7 +1,19 @@ - import { createBrowserHistory, createMemoryHistory } from 'history' -export const router = +const history = typeof window === 'undefined' ? createMemoryHistory() : createBrowserHistory({}) + + +export const router = { + // createHref: history.createHref, + location: history.location, + push: history.push, + replace: history.replace, + go: history.go, + back: history.back, + forward: history.forward, + listen: history.listen, + block: history.block, +} diff --git a/spiceflow/src/vite.tsx b/spiceflow/src/vite.tsx index 3507068..6b4b65e 100644 --- a/spiceflow/src/vite.tsx +++ b/spiceflow/src/vite.tsx @@ -68,7 +68,7 @@ export function spiceflowPlugin({ entry }): PluginOption { } const mod = await server?.environments.client.moduleGraph.getModuleById(id) - // console.log('mod', id, mod?.lastHMRTimestamp) + // console.log('mod', id, mod?.lastHMRTimestamp, ) // console.log([...server?.moduleGraph.idToModuleMap.keys()]) let generateId = (id) => { let generated = '' From 95ee6c0249c830fb95807e048552ba4c7ba8b678 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 22:49:17 +0100 Subject: [PATCH 057/100] managed to catch errors in rsc, cool, fix page ordering with :id --- example-react/src/app/client.tsx | 16 ++++++---------- example-react/src/main.tsx | 12 ++++++++++-- spiceflow/src/spiceflow.tsx | 4 +++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index 1c19044..c3cdbdb 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -1,5 +1,5 @@ "use client"; -import './client.css' +import "./client.css"; import React from "react"; import { add } from "./action-by-client"; @@ -58,20 +58,16 @@ export function Calculator() { ={returnValue ?? "?"} - ); } - - - export function ClientComponentThrows() { - throw new Error('Client component error'); + throw new Error("Client component error"); return
      Client component
      ; } - - - - +export function ErrorRender({ error }) { + console.log("caught error", error); + return
      Error from rsc
      ; +} diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index a162332..9d9384e 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -4,7 +4,7 @@ import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; import "./styles.css"; -import { ClientComponentThrows } from "./app/client"; +import { ClientComponentThrows, ErrorRender } from "./app/client"; import { ErrorBoundary } from "spiceflow/dist/react/components"; import { redirect, sleep } from "spiceflow/dist/utils"; @@ -116,7 +116,14 @@ const app = new Spiceflow() ); }) - .page("/loader-error", async () => { + .layout("/error-boundary", async ({ children }) => { + return ( + + {children} + + ); + }) + .page("/error-boundary", async () => { throw new Error("test error"); }) .page("/rsc-error", async () => { @@ -143,3 +150,4 @@ async function ServerComponentThrows() { } export default app; + diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index a5bcdaf..9f532b4 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -861,7 +861,9 @@ export class Spiceflow< reactRoutes, (x) => x.route.kind === 'page', ) - const pageRoute = pageRoutes[0] + const pageRoute = pageRoutes.sort((a, b) => { + return routeSorter(a.route, b.route) + })[0] if (!pageRoute) { // TODO customize not found route return new Response('Not Found', { status: 404 }) From f2cc826ace3fb36f83d217f3788debad3415b0dc Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 23:45:08 +0100 Subject: [PATCH 058/100] added not found function --- example-react/e2e/basic.test.ts | 22 +++++++- example-react/src/main.tsx | 16 ++++-- spiceflow/src/react/components.tsx | 83 +++++++++++++++++++++++++++- spiceflow/src/react/entry.client.tsx | 11 ++-- spiceflow/src/react/entry.ssr.tsx | 11 ++-- spiceflow/src/react/errors.tsx | 6 ++ spiceflow/src/spiceflow.tsx | 49 +++++++++++++++- 7 files changed, 177 insertions(+), 21 deletions(-) diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index 38aa93b..4c8fe62 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -1,6 +1,20 @@ import { type Page, expect, test } from "@playwright/test"; import { createEditor } from "./helper.js"; +test.describe("not found", () => { + test("not found in outer route scope", async ({ page }) => { + await page.goto("/not-found"); + await expect(page.getByText("404")).toBeVisible(); + await expect(page.getByText("This page could not be found.")).toBeVisible(); + }); + + test("not found in RSC inside suspense", async ({ page }) => { + await page.goto("/not-found-in-suspense"); + await expect(page.getByText("404")).toBeVisible(); + await expect(page.getByText("This page could not be found.")).toBeVisible(); + }); +}); + test.describe("redirect", () => { test("redirect in outer route scope", async ({ page }) => { await page.goto("/top-level-redirect"); @@ -12,12 +26,16 @@ test.describe("redirect", () => { await expect(page).toHaveURL("/"); await page.getByText("[hydrated: 1]").click(); }); - test("redirect in RSC, slow (meaning not first rsc chunk)", async ({ page }) => { + test("redirect in RSC, slow (meaning not first rsc chunk)", async ({ + page, + }) => { await page.goto("/slow-redirect"); await expect(page).toHaveURL("/"); await page.getByText("[hydrated: 1]").click(); }); - test("redirect in RSC inside suspense, redirect made by client", async ({ page }) => { + test("redirect in RSC inside suspense, redirect made by client", async ({ + page, + }) => { await page.goto("/redirect-in-rsc-suspense"); await expect(page).toHaveURL("/"); await page.getByText("[hydrated: 1]").click(); diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 9d9384e..840c109 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -7,6 +7,7 @@ import "./styles.css"; import { ClientComponentThrows, ErrorRender } from "./app/client"; import { ErrorBoundary } from "spiceflow/dist/react/components"; import { redirect, sleep } from "spiceflow/dist/utils"; +import { notFound } from "spiceflow/dist/react/errors"; const app = new Spiceflow() .layout("/*", async ({ children, request }) => { @@ -28,6 +29,16 @@ const app = new Spiceflow() }) .get("/hello", () => "Hello, World!") + .page("/not-found", () => { + throw notFound(); + }) + .layout("/not-found-in-suspense", async ({ children }) => { + return not found...}>{children}; + }) + .page("/not-found-in-suspense", async () => { + await sleep(100); + throw notFound(); + }) .page("/top-level-redirect", async () => { throw redirect("/"); }) @@ -118,9 +129,7 @@ const app = new Spiceflow() }) .layout("/error-boundary", async ({ children }) => { return ( - - {children} - + {children} ); }) .page("/error-boundary", async () => { @@ -150,4 +159,3 @@ async function ServerComponentThrows() { } export default app; - diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index ac573b4..2523a1f 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -7,7 +7,6 @@ import { ServerPayload } from '../spiceflow.js' import { isRedirectError, isNotFoundError, getErrorContext } from './errors.js' import { useFlightData } from './context.js' - export function LayoutContent(props: { id?: string }) { const data = useFlightData() if (!data) return null @@ -80,9 +79,10 @@ class ErrorBoundary_ extends React.Component { if (ctx && isRedirectError(ctx) && ctx.headers?.['location']) { console.log('redirecting from browser to', ctx.headers?.['location']) router.replace(ctx.headers?.['location']) + return {} } if (ctx && isNotFoundError(ctx)) { - // TODO somehow show the not found page + throw error } return { error } } @@ -131,6 +131,7 @@ export function DefaultGlobalErrorPage(props: ErrorPageProps) { : `Unknown Client Error (see browser console for the details)` return ( + {message} ) { /> ) } + +// https://github.com/vercel/next.js/blob/c74f3f54b23b3fc47dc7e214a8949844257a734a/packages/next/src/build/webpack/loaders/next-app-loader.ts#L72 +// https://github.com/vercel/next.js/blob/8f5f0ef141a907d083eedb7c7aca52b04f9d258b/packages/next/src/client/components/not-found-error.tsx#L34-L39 +export function DefaultNotFoundPage() { + return ( + + + not found + +
      +
      +

      + 404 +

      +

      + This page could not be found. +

      +
      +
      + + + ) +} +export class NotFoundBoundary extends React.Component<{ + component: React.ComponentType + children?: React.ReactNode +}> { + override state: { error?: Error } = {} + + static getDerivedStateFromError(error: Error) { + const ctx = getErrorContext(error) + if (ctx && isNotFoundError(ctx)) { + return { error } + } + throw error + } + + override render() { + if (this.state.error) { + const Component = this.props.component + return ( + <> + + { + React.startTransition(() => { + this.setState({ error: null }) + }) + }} + /> + + ) + } + return this.props.children + } +} diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 363ce72..4e23121 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -8,9 +8,10 @@ import { clientReferenceManifest } from './utils/client-reference.js' import { rscStream } from 'rsc-html-stream/client' import { DefaultGlobalErrorPage, + DefaultNotFoundPage, ErrorBoundary, - LayoutContent, + NotFoundBoundary, } from './components.js' import { ServerPayload } from '../spiceflow.js' import { FlightDataContext } from './context.js' @@ -68,9 +69,11 @@ async function main() { return ( - - - + + + + + ) } diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index f013c39..fb25e43 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -6,7 +6,7 @@ import type { ModuleRunner } from 'vite/module-runner' import { injectRSCPayload } from 'rsc-html-stream/server' import cssUrls from 'virtual:app-styles' import { ServerPayload } from '../spiceflow.js' -import { LayoutContent } from './components.js' +import { DefaultNotFoundPage, LayoutContent } from './components.js' import { clientReferenceManifest } from './utils/client-reference.js' import { createRequest, @@ -68,6 +68,7 @@ export default async function handler( }, }) } catch (e) { + status = 500 console.log(`error during ssr render catch`, e) let errCtx = getErrorContext(e) if (errCtx && isRedirectError(errCtx)) { @@ -81,12 +82,9 @@ export default async function handler( ) return } + let content: any = null if (errCtx && isNotFoundError(errCtx)) { - // TODO show a not found component instead - sendResponse( - new Response('404', { status: errCtx.status, headers: errCtx.headers }), - res, - ) + status = 404 return } // https://bsky.app/profile/ebey.bsky.social/post/3lev4lqr2ak2j @@ -103,6 +101,7 @@ export default async function handler( + {content} ) diff --git a/spiceflow/src/react/errors.tsx b/spiceflow/src/react/errors.tsx index 9561a8a..1ff72f4 100644 --- a/spiceflow/src/react/errors.tsx +++ b/spiceflow/src/react/errors.tsx @@ -30,6 +30,12 @@ export function redirect( }) } +export function notFound() { + return createError({ + status: 404, + }) +} + export function isRedirectError(ctx?: ReactServerErrorContext) { if (!ctx) return false const location = ctx.headers?.['location'] diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index 9f532b4..4ddb02c 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -38,7 +38,11 @@ import { isProduction, ValidationError } from './error.js' import { isAsyncIterable, isResponse, redirect } from './utils.js' import { PassThrough, Readable } from 'stream' -import { FlightData, LayoutContent } from './react/components.js' +import { + DefaultNotFoundPage, + FlightData, + LayoutContent, +} from './react/components.js' import { ClientReferenceMetadataManifest, ServerReferenceManifest, @@ -46,7 +50,11 @@ import { import { TrieRouter } from './trie-router/router.js' import { decodeURIComponent_ } from './trie-router/url.js' import { Result } from './trie-router/utils.js' -import { isRedirectError } from './react/errors.js' +import { + getErrorContext, + isNotFoundError, + isRedirectError, +} from './react/errors.js' const ajv = (addFormats.default || addFormats)( new (Ajv.default || Ajv)({ useDefaults: true }), @@ -975,7 +983,9 @@ export class Spiceflow< request.signal.addEventListener('abort', () => { abortable.abort() }) - const passthrough = new PassThrough() + const passthrough = new PassThrough({ + writableHighWaterMark: 1024 * 1024, + }) const nodeStream = abortable.pipe(passthrough) const stream = Readable.toWeb(nodeStream) as ReadableStream @@ -991,6 +1001,39 @@ export class Spiceflow< if (thrownError instanceof Response) { return thrownError } + let errCtx = getErrorContext(thrownError) + if (errCtx && isRedirectError(errCtx)) { + console.log(`redirecting to ${errCtx.headers?.location}`) + return new Response(errCtx.headers?.location, { + status: errCtx.status, + headers: errCtx.headers, + }) + } + if (errCtx && isNotFoundError(errCtx)) { + console.log(`not found error for ${request.url}`) + let el = + let htmlAbortable = await ReactServer.renderToPipeableStream( + { + root: { + page: el, + layouts: [], + }, + returnValue, + formState, + }, + clientReferenceMetadataManifest, + ) + const htmlStream = Readable.toWeb( + htmlAbortable.pipe(new PassThrough()), + ) as ReadableStream + + return new Response(htmlStream, { + status: 404, + headers: { + 'content-type': 'text/x-component;charset=utf-8', + }, + }) + } return new Response(stream, { headers: { From 447ac5ba768f4bd7c244b36ac68f45da7e0c1e28 Mon Sep 17 00:00:00 2001 From: remorses Date: Tue, 11 Feb 2025 23:55:52 +0100 Subject: [PATCH 059/100] refactored renderReact --- spiceflow/src/spiceflow.tsx | 372 ++++++++++++++++++------------------ 1 file changed, 189 insertions(+), 183 deletions(-) diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index 4ddb02c..3460517 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -842,6 +842,187 @@ export class Spiceflow< return this } + async renderReact({ + request, + reactRoutes, + defaultContext, + }: { + request: Request + defaultContext + reactRoutes: Array<{ + route: InternalRoute + app: AnySpiceflow + params: Record + }> + }) { + const ReactServer = await import( + 'spiceflow/dist/react/server-dom-optimized' + ).then((m) => m.default) + const [pageRoutes, layoutRoutes] = partition( + reactRoutes, + (x) => x.route.kind === 'page', + ) + const pageRoute = pageRoutes.sort((a, b) => { + return routeSorter(a.route, b.route) + })[0] + if (!pageRoute) { + // TODO customize not found route + return new Response('Not Found', { status: 404 }) + } + const kind = pageRoute?.route?.kind + + let Page = pageRoute?.route?.handler as any + let page = ( + + ) + const layouts = layoutRoutes.map((layout) => { + const id = layout.route.id + const children = createElement(LayoutContent, { id }) + + let Layout = layout.route.handler as any + const element = ( + + ) + return { element, id } + }) + + let root: FlightData = { + url: request.url, + page, + layouts, + } + let returnValue: unknown | undefined + let formState: ReactFormState | undefined + if (request.method === 'POST') { + const url = new URL(request.url) + const actionId = url.searchParams.get('__rsc') + if (actionId) { + // client stream request + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + const args = await ReactServer.decodeReply(body) + const reference = + serverReferenceManifest.resolveServerReference(actionId) + await reference.preload() + const action = await reference.get() + // TODO handle action errors, redirects, etc + returnValue = await (action as any).apply(null, args) + } else { + // progressive enhancement + const formData = await request.formData() + console.log(formData) + const decodedAction = await ReactServer.decodeAction( + formData, + serverReferenceManifest, + ) + formState = await ReactServer.decodeFormState( + await decodedAction(), + formData, + serverReferenceManifest, + ) + } + } + + if (root instanceof Response) { + return root + } + + let thrownError + let abortable = ReactServer.renderToPipeableStream( + { + root, + returnValue, + formState, + }, + clientReferenceMetadataManifest, + { + onError(error) { + console.error('[spiceflow:renderToPipeableStream]', error) + thrownError = error + return error?.digest || error?.message + }, + }, + ) + request.signal.addEventListener('abort', () => { + abortable.abort() + }) + const passthrough = new PassThrough({ + writableHighWaterMark: 1024 * 1024, + }) + const nodeStream = abortable.pipe(passthrough) + const stream = Readable.toWeb(nodeStream) as ReadableStream + + const timerId = `wait for first chunk ${Math.random().toString(36).slice(2)}` + console.time(timerId) + await new Promise((resolve) => { + passthrough.once('data', () => { + resolve() + }) + }) + console.timeEnd(timerId) + + if (thrownError instanceof Response) { + return thrownError + } + let errCtx = getErrorContext(thrownError) + if (errCtx && isRedirectError(errCtx)) { + console.log(`redirecting to ${errCtx.headers?.location}`) + return new Response(errCtx.headers?.location, { + status: errCtx.status, + headers: errCtx.headers, + }) + } + if (errCtx && isNotFoundError(errCtx)) { + console.log(`not found error for ${request.url}`) + let el = + let htmlAbortable = await ReactServer.renderToPipeableStream( + { + root: { + page: el, + layouts: [], + }, + returnValue, + formState, + }, + clientReferenceMetadataManifest, + ) + const htmlStream = Readable.toWeb( + htmlAbortable.pipe(new PassThrough()), + ) as ReadableStream + + return new Response(htmlStream, { + status: 404, + headers: { + 'content-type': 'text/x-component;charset=utf-8', + }, + }) + } + + return new Response(stream, { + headers: { + 'content-type': 'text/x-component;charset=utf-8', + }, + }) + } + async handle(request: Request) { let u = new URL(request.url, 'http://localhost') const self = this @@ -850,6 +1031,8 @@ export class Spiceflow< redirect, error: null, children: undefined, + query: parseQuery((u.search || '').slice(1)), + request, path, } const root = this.topLevelApp || this @@ -862,194 +1045,19 @@ export class Spiceflow< (x) => !x.route.kind, ) if (reactRoutes.length) { - const ReactServer = await import( - 'spiceflow/dist/react/server-dom-optimized' - ).then((m) => m.default) - const [pageRoutes, layoutRoutes] = partition( + const res = await this.renderReact({ + request, + defaultContext, reactRoutes, - (x) => x.route.kind === 'page', - ) - const pageRoute = pageRoutes.sort((a, b) => { - return routeSorter(a.route, b.route) - })[0] - if (!pageRoute) { - // TODO customize not found route - return new Response('Not Found', { status: 404 }) - } - const kind = pageRoute?.route?.kind - - try { - const baseContext = { - ...defaultContext, - request, - - path, - query: parseQuery((u.search || '').slice(1)), - // params: _params, - redirect, - } - - let Page = pageRoute?.route?.handler as any - let page = ( - - ) - const layouts = layoutRoutes.map((layout) => { - const id = layout.route.id - const children = createElement(LayoutContent, { id }) - - let Layout = layout.route.handler as any - const element = ( - - ) - return { element, id } - }) - - let root: FlightData = { - url: request.url, - page, - layouts, - } - let returnValue: unknown | undefined - let formState: ReactFormState | undefined - if (request.method === 'POST') { - const url = new URL(request.url) - const actionId = url.searchParams.get('__rsc') - if (actionId) { - // client stream request - const contentType = request.headers.get('content-type') - const body = contentType?.startsWith('multipart/form-data') - ? await request.formData() - : await request.text() - const args = await ReactServer.decodeReply(body) - const reference = - serverReferenceManifest.resolveServerReference(actionId) - await reference.preload() - const action = await reference.get() - // TODO handle action errors, redirects, etc - returnValue = await (action as any).apply(null, args) - } else { - // progressive enhancement - const formData = await request.formData() - console.log(formData) - const decodedAction = await ReactServer.decodeAction( - formData, - serverReferenceManifest, - ) - formState = await ReactServer.decodeFormState( - await decodedAction(), - formData, - serverReferenceManifest, - ) - } - } - - if (root instanceof Response) { - return root - } - - let thrownError - let abortable = ReactServer.renderToPipeableStream( - { - root, - returnValue, - formState, - }, - clientReferenceMetadataManifest, - { - onError(error) { - console.error('[spiceflow:renderToPipeableStream]', error) - thrownError = error - return error?.digest || error?.message - }, - }, - ) - request.signal.addEventListener('abort', () => { - abortable.abort() - }) - const passthrough = new PassThrough({ - writableHighWaterMark: 1024 * 1024, - }) - const nodeStream = abortable.pipe(passthrough) - const stream = Readable.toWeb(nodeStream) as ReadableStream - - const timerId = `wait for first chunk ${Math.random().toString(36).slice(2)}` - console.time(timerId) - await new Promise((resolve) => { - passthrough.once('data', () => { - resolve() - }) - }) - console.timeEnd(timerId) - - if (thrownError instanceof Response) { - return thrownError - } - let errCtx = getErrorContext(thrownError) - if (errCtx && isRedirectError(errCtx)) { - console.log(`redirecting to ${errCtx.headers?.location}`) - return new Response(errCtx.headers?.location, { - status: errCtx.status, - headers: errCtx.headers, - }) - } - if (errCtx && isNotFoundError(errCtx)) { - console.log(`not found error for ${request.url}`) - let el = - let htmlAbortable = await ReactServer.renderToPipeableStream( - { - root: { - page: el, - layouts: [], - }, - returnValue, - formState, - }, - clientReferenceMetadataManifest, - ) - const htmlStream = Readable.toWeb( - htmlAbortable.pipe(new PassThrough()), - ) as ReadableStream - - return new Response(htmlStream, { - status: 404, - headers: { - 'content-type': 'text/x-component;charset=utf-8', - }, - }) - } - - return new Response(stream, { - headers: { - 'content-type': 'text/x-component;charset=utf-8', - }, - }) - } catch (err) { - return await getResForError(err) - } + }) + return res } const route = nonReactRoutes.sort((a, b) => { return routeSorter(a.route, b.route) })[0] // TODO get all apps in scope? layouts can match between apps when using .use? - const appsInScope = this.getAppsInScope(routes[0].app) + const appsInScope = this.getAppsInScope(route.app) onErrorHandlers = appsInScope.flatMap((x) => x.onErrorHandlers) let { params: _params, @@ -1076,8 +1084,6 @@ export class Spiceflow< ...defaultContext, request, state, - path, - query: parseQuery((u.search || '').slice(1)), params: _params, redirect, } satisfies MiddlewareContext From 77d815707f87be6d555aa6ab3344e696624695bb Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 12 Feb 2025 00:19:34 +0100 Subject: [PATCH 060/100] adding middleware support for react too, works pretty well, hope performance stays good --- example-react/e2e/basic.test.ts | 10 ++++ example-react/src/main.tsx | 19 ++++++- spiceflow/src/context.ts | 4 +- spiceflow/src/react/entry.ssr.tsx | 10 +++- spiceflow/src/spiceflow.tsx | 86 +++++++++++++++++++------------ spiceflow/src/utils.ts | 2 + 6 files changed, 94 insertions(+), 37 deletions(-) diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index 4c8fe62..cc6c825 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -14,6 +14,16 @@ test.describe("not found", () => { await expect(page.getByText("This page could not be found.")).toBeVisible(); }); }); +test.describe("middleware with use()", () => { + test("middleware sets response header", async ({ page }) => { + const response = await page.goto("/"); + expect(response?.headers()["x-middleware-1"]).toBe("ok"); + }); + test("middleware sets state", async ({ page }) => { + await page.goto("/state"); + await expect(page.getByText("state set by middleware1")).toBeVisible(); + }); +}); test.describe("redirect", () => { test("redirect in outer route scope", async ({ page }) => { diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 840c109..2677a8f 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -10,7 +10,16 @@ import { redirect, sleep } from "spiceflow/dist/utils"; import { notFound } from "spiceflow/dist/react/errors"; const app = new Spiceflow() - .layout("/*", async ({ children, request }) => { + .state("middleware1", "") + .use(async ({ request, state }, next) => { + console.log("middleware 1"); + state.middleware1 = "state set by middleware1"; + const res = await next(); + res.headers.set("x-middleware-1", "ok"); + console.log("middleware 2"); + return res; + }) + .layout("/*", async ({ children, state }) => { return ( title from layout @@ -18,6 +27,14 @@ const app = new Spiceflow() ); }) + .page("/state", async ({ state }) => { + return ( + <> + title from page + state: {state.middleware1} + + ); + }) .page("/", async ({ request }) => { const url = new URL(request.url); return ( diff --git a/spiceflow/src/context.ts b/spiceflow/src/context.ts index 20aa9e6..1298fa9 100644 --- a/spiceflow/src/context.ts +++ b/spiceflow/src/context.ts @@ -1,7 +1,7 @@ import type { StatusMap, InvertedStatusMap, - redirect as Redirect, + Redirect, } from './utils.js' import type { @@ -98,7 +98,7 @@ export type MiddlewareContext< path: string query?: Record params?: Record - + redirect: Redirect // server: Server | null diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index fb25e43..98bef81 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -68,7 +68,7 @@ export default async function handler( }, }) } catch (e) { - status = 500 + status = 500 console.log(`error during ssr render catch`, e) let errCtx = getErrorContext(e) if (errCtx && isRedirectError(errCtx)) { @@ -76,7 +76,11 @@ export default async function handler( sendResponse( new Response(errCtx.headers?.location, { status: errCtx.status, - headers: errCtx.headers, + headers: { + ...Object.fromEntries(response.headers), + ...errCtx.headers, + contentType: 'text/html', + }, }), res, ) @@ -116,6 +120,8 @@ export default async function handler( { status, headers: { + // copy rsc headers, so spiceflow can add its own headers via .use() + ...Object.fromEntries(response.headers), 'content-type': 'text/html;charset=utf-8', }, }, diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index 3460517..e8f346a 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -33,7 +33,7 @@ import Ajv, { ValidateFunction } from 'ajv' import { createElement } from 'react' import { z, ZodType } from 'zod' import { zodToJsonSchema } from 'zod-to-json-schema' -import { MiddlewareContext } from './context.js' +import { Context, MiddlewareContext } from './context.js' import { isProduction, ValidationError } from './error.js' import { isAsyncIterable, isResponse, redirect } from './utils.js' @@ -845,10 +845,10 @@ export class Spiceflow< async renderReact({ request, reactRoutes, - defaultContext, + context, }: { request: Request - defaultContext + context reactRoutes: Array<{ route: InternalRoute app: AnySpiceflow @@ -875,9 +875,7 @@ export class Spiceflow< let page = ( @@ -890,10 +888,7 @@ export class Spiceflow< const element = ( const root = this.topLevelApp || this let onErrorHandlers: OnError[] = [] @@ -1044,13 +1038,49 @@ export class Spiceflow< routes, (x) => !x.route.kind, ) + let index = 0 if (reactRoutes.length) { - const res = await this.renderReact({ - request, - defaultContext, - reactRoutes, - }) - return res + const appsInScope = this.getAppsInScope(reactRoutes[0].app) + onErrorHandlers = appsInScope.flatMap((x) => x.onErrorHandlers) + const middlewares = appsInScope.flatMap((x) => x.middlewares) + let handlerResponse: Response | undefined + + let context = defaultContext + const next = async () => { + try { + if (index < middlewares.length) { + const middleware = middlewares[index] + index++ + + const result = await middleware(context, next) + if (isResponse(result)) { + handlerResponse = result + } + if (!result && index < middlewares.length) { + return await next() + } else if (result) { + return await turnHandlerResultIntoResponse(result) + } + } + if (handlerResponse) { + return handlerResponse + } + + const res = await this.renderReact({ + request, + context, + reactRoutes, + }) + + return res + } catch (err) { + handlerResponse = await getResForError(err) + return await next() + } + } + const response = await next() + + return response } const route = nonReactRoutes.sort((a, b) => { return routeSorter(a.route, b.route) @@ -1059,13 +1089,11 @@ export class Spiceflow< // TODO get all apps in scope? layouts can match between apps when using .use? const appsInScope = this.getAppsInScope(route.app) onErrorHandlers = appsInScope.flatMap((x) => x.onErrorHandlers) + const middlewares = appsInScope.flatMap((x) => x.middlewares) let { params: _params, app: { defaultState }, } = route - const middlewares = appsInScope.flatMap((x) => x.middlewares) - - let state = cloneDeep(defaultState) let content = route?.route?.hooks?.content @@ -1079,14 +1107,8 @@ export class Spiceflow< request = typedRequest } - let index = 0 - let context = { - ...defaultContext, - request, - state, - params: _params, - redirect, - } satisfies MiddlewareContext + let context: any = defaultContext + context.params = _params let handlerResponse: Response | undefined async function getResForError(err: any) { @@ -1579,7 +1601,7 @@ export function bfs(tree: AnySpiceflow) { export async function turnHandlerResultIntoResponse( result: any, - route: InternalRoute, + route?: InternalRoute, ) { // if user returns a promise, await it if (result instanceof Promise) { @@ -1590,7 +1612,7 @@ export async function turnHandlerResultIntoResponse( return result } - if (route.type) { + if (route?.type) { if (route.type?.includes('multipart/form-data')) { if (!(result instanceof Response)) { throw new Error( diff --git a/spiceflow/src/utils.ts b/spiceflow/src/utils.ts index 0677b1f..9d3c6b4 100644 --- a/spiceflow/src/utils.ts +++ b/spiceflow/src/utils.ts @@ -30,6 +30,8 @@ export function sleep(ms: number) { export { redirect } +export type Redirect = typeof redirect + export const StatusMap = { Continue: 100, 'Switching Protocols': 101, From 312a69bf86788cad1737ef48f2fe1c1b31a42fa1 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 12 Feb 2025 00:38:20 +0100 Subject: [PATCH 061/100] fix validation errors, forgot to change request in context --- spiceflow/src/react.test.ts | 131 -------------------------------- spiceflow/src/spiceflow.test.ts | 8 +- spiceflow/src/spiceflow.tsx | 15 ++-- 3 files changed, 10 insertions(+), 144 deletions(-) delete mode 100644 spiceflow/src/react.test.ts diff --git a/spiceflow/src/react.test.ts b/spiceflow/src/react.test.ts deleted file mode 100644 index f9ae1eb..0000000 --- a/spiceflow/src/react.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { test, describe, expect } from 'vitest' -import { Type } from '@sinclair/typebox' -import { bfs, cloneDeep, Spiceflow } from './spiceflow.js' -import { z } from 'zod' -import { createSpiceflowClient } from './client/index.js' - -test('layout and page work together', async () => { - const res = await new Spiceflow() - .layout('/xxx', () => ({ layout: 'layout' })) - .page('/xxx', () => ({ page: 'page' })) - .handle(new Request('http://localhost/xxx', { method: 'POST' })) - - expect(res).toMatchInlineSnapshot(` - { - "layouts": [ - { - "element": { - "layout": "layout", - }, - "id": "layout-post--xxx", - }, - ], - "page": { - "page": "page", - }, - "url": "http://localhost/xxx", - } - `) -}) -test('layout and page, static routes have priority', async () => { - const res = await new Spiceflow() - .layout('/xxx', () => ({ layout: 'layout' })) - .page('/:id', () => ({ page: ':id' })) - .page('/xxx', () => ({ page: 'page' })) - .handle(new Request('http://localhost/xxx', { method: 'POST' })) - - expect(res).toMatchInlineSnapshot(` - { - "layouts": [ - { - "element": { - "layout": "layout", - }, - "id": "layout-post--xxx", - }, - ], - "page": { - "page": "page", - }, - "url": "http://localhost/xxx", - } - `) -}) - -test('layout and page work together with params', async () => { - const app = new Spiceflow() - .layout('/', async ({ children }) => ({ layout: 'root', children })) - .page('/:id', async ({ params }) => ({ page: params.id })) - - const routes = app.getAllRoutes() - expect(routes).toMatchInlineSnapshot(` - [ - { - "handler": [Function], - "hooks": undefined, - "id": "layout-get--", - "kind": "layout", - "method": "GET", - "path": "/", - "type": "", - "validateBody": undefined, - "validateParams": undefined, - "validateQuery": undefined, - }, - { - "handler": [Function], - "hooks": undefined, - "id": "layout-post--", - "kind": "layout", - "method": "POST", - "path": "/", - "type": "", - "validateBody": undefined, - "validateParams": undefined, - "validateQuery": undefined, - }, - { - "handler": [Function], - "hooks": undefined, - "id": "page-get--:id", - "kind": "page", - "method": "GET", - "path": "/:id", - "type": "", - "validateBody": undefined, - "validateParams": undefined, - "validateQuery": undefined, - }, - { - "handler": [Function], - "hooks": undefined, - "id": "page-post--:id", - "kind": "page", - "method": "POST", - "path": "/:id", - "type": "", - "validateBody": undefined, - "validateParams": undefined, - "validateQuery": undefined, - }, - ] - `) - - const res = await app.handle(new Request('http://localhost/123')) - - expect(app.router).toMatchInlineSnapshot(` - TrieRouter { - "name": "TrieRouter", - } - `) - - expect(await res).toMatchInlineSnapshot(` - { - "layouts": [], - "page": { - "page": "123", - }, - "url": "http://localhost/123", - } - `) -}) diff --git a/spiceflow/src/spiceflow.test.ts b/spiceflow/src/spiceflow.test.ts index 9342b6b..e70fe07 100644 --- a/spiceflow/src/spiceflow.test.ts +++ b/spiceflow/src/spiceflow.test.ts @@ -515,10 +515,10 @@ test('validate body works, request fails', async () => { expect(body).toEqual({ name: 'John' }) }, { - body: Type.Object({ - name: Type.String(), - requiredField: Type.String(), - }), + body: z.object({ + name: z.string(), + requiredField: z.string(), + }).strict(), }, ) .handle( diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index e8f346a..f0e00f9 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -1022,13 +1022,14 @@ export class Spiceflow< let u = new URL(request.url, 'http://localhost') const self = this let path = u.pathname - const defaultContext = { + const context = { redirect, state: cloneDeep(this.defaultState), query: parseQuery((u.search || '').slice(1)), request, path, - } satisfies MiddlewareContext + params: {}, + } const root = this.topLevelApp || this let onErrorHandlers: OnError[] = [] @@ -1045,7 +1046,6 @@ export class Spiceflow< const middlewares = appsInScope.flatMap((x) => x.middlewares) let handlerResponse: Response | undefined - let context = defaultContext const next = async () => { try { if (index < middlewares.length) { @@ -1090,10 +1090,7 @@ export class Spiceflow< const appsInScope = this.getAppsInScope(route.app) onErrorHandlers = appsInScope.flatMap((x) => x.onErrorHandlers) const middlewares = appsInScope.flatMap((x) => x.middlewares) - let { - params: _params, - app: { defaultState }, - } = route + let { params: _params } = route let content = route?.route?.hooks?.content @@ -1105,10 +1102,10 @@ export class Spiceflow< : new SpiceflowRequest(u, request) typedRequest.validateBody = route?.route?.validateBody request = typedRequest + context.request = typedRequest } - let context: any = defaultContext - context.params = _params + context['params'] = _params let handlerResponse: Response | undefined async function getResForError(err: any) { From eec8ad69934d2a386cf1c04a013460d752dd5eb8 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 12 Feb 2025 00:39:40 +0100 Subject: [PATCH 062/100] not found --- spiceflow/src/spiceflow.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index f0e00f9..0b1b049 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -1115,6 +1115,11 @@ export class Spiceflow< headers: err.headers, }) } + if (isNotFoundError(err)) { + return new Response(JSON.stringify('not found'), { + status: 404, + }) + } if (isResponse(err)) return err let res = await self.runErrorHandlers({ onErrorHandlers, From c346438ad3c68629c3a1a207426093c57a32321e Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 12 Feb 2025 12:26:37 +0100 Subject: [PATCH 063/100] show error overlay on rsc errors --- spiceflow/src/react/components.tsx | 3 +++ spiceflow/src/react/entry.client.tsx | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 2523a1f..a0894a6 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -84,6 +84,9 @@ class ErrorBoundary_ extends React.Component { if (ctx && isNotFoundError(ctx)) { throw error } + if (import.meta.env.DEV) { + throw error + } return { error } } diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index 4e23121..9fb9108 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -1,4 +1,5 @@ import React, { Suspense } from 'react' +import type { ErrorPayload } from 'vite' import { router } from './router.js' import ReactDomClient from 'react-dom/client' import ReactClient from 'spiceflow/dist/react/server-dom-client-optimized' @@ -90,4 +91,17 @@ async function main() { } } +if (import.meta.env.DEV) { + window.onerror = (event, source, lineno, colno, err) => { + // must be within function call because that's when the element is defined for sure. + const ErrorOverlay = customElements.get('vite-error-overlay') + // don't open outside vite environment + if (!ErrorOverlay) { + return + } + const overlay = new ErrorOverlay(err) + document.body.appendChild(overlay) + } +} + main() From 8d0798733487f985509620a1cc18720ecbfc38ee Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 12 Feb 2025 12:30:45 +0100 Subject: [PATCH 064/100] tested errors in useEffect --- example-react/src/app/client.tsx | 9 +++++++++ example-react/src/main.tsx | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index c3cdbdb..4b6c5ae 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -17,6 +17,15 @@ export function Counter() { ); } +export function ErrorInUseEffect() { + React.useEffect(() => { + setTimeout(() => { + throw new Error("Error in useEffect"); + }, 0); + }, []); + return
      ErrorInUseEffect
      ; +} + export function Hydrated() { return
      [hydrated: {Number(useHydrated())}]
      ; } diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 2677a8f..ff8876b 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -4,7 +4,11 @@ import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; import "./styles.css"; -import { ClientComponentThrows, ErrorRender } from "./app/client"; +import { + ClientComponentThrows, + ErrorInUseEffect, + ErrorRender, +} from "./app/client"; import { ErrorBoundary } from "spiceflow/dist/react/components"; import { redirect, sleep } from "spiceflow/dist/utils"; import { notFound } from "spiceflow/dist/react/errors"; @@ -152,6 +156,9 @@ const app = new Spiceflow() .page("/error-boundary", async () => { throw new Error("test error"); }) + .page("/error-in-use-effect", async () => { + return ; + }) .page("/rsc-error", async () => { return ; }) From 5c1e69e2dd5ae2ae8d030b0c9676deffd0e94fb5 Mon Sep 17 00:00:00 2001 From: remorses Date: Wed, 12 Feb 2025 15:25:29 +0100 Subject: [PATCH 065/100] use @jacob-ebey/react-server-dom-vite directly because of pnpm bug, added @jacob-ebey/react-server-dom-vite which does not work --- example-react/package.json | 14 +- example-react/src/app/chakra.tsx | 22 + example-react/src/app/dialog.tsx | 65 + example-react/src/app/select.tsx | 15 + example-react/src/main.tsx | 12 + example-react/vite.config.ts | 8 + pnpm-lock.yaml | 3661 ++++++++++++++--- spiceflow/package.json | 4 +- spiceflow/src/react/entry.rsc.tsx | 9 - spiceflow/src/react/references.browser.tsx | 2 +- .../src/react/server-dom-client-optimized.tsx | 2 +- spiceflow/src/react/server-dom-optimized.tsx | 2 +- spiceflow/src/react/types/ambient.d.ts | 2 +- spiceflow/src/vite.tsx | 143 +- 14 files changed, 3438 insertions(+), 523 deletions(-) create mode 100644 example-react/src/app/chakra.tsx create mode 100644 example-react/src/app/dialog.tsx create mode 100644 example-react/src/app/select.tsx diff --git a/example-react/package.json b/example-react/package.json index a975b1e..5853489 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -5,21 +5,25 @@ "main": "index.js", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build --app", - "preview": "vite preview", - "test-e2e": "playwright test", - "test-e2e-preview": "E2E_PREVIEW=1 playwright test" + "dev": "DEBUG_SPICEFLOW=1 vite", + "build": "DEBUG_SPICEFLOW=1 vite build --app", + "preview": "DEBUG_SPICEFLOW=1 vite preview", + "test-e2e": "DEBUG_SPICEFLOW=1 playwright test", + "test-e2e-preview": "DEBUG_SPICEFLOW=1 E2E_PREVIEW=1 playwright test" }, "keywords": [], "author": "remorses ", "dependencies": { + "@chakra-ui/react": "^3.8.0", "@playwright/test": "^1.50.1", + "@radix-ui/react-icons": "^1.3.2", "@tailwindcss/vite": "^4.0.5", "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", + "radix-ui": "^1.1.3", "react": "19.0.0", "react-dom": "19.0.0", + "react-select": "^5.10.0", "spiceflow": "workspace:*", "tailwindcss": "^4.0.5", "vite": "^6.1.0" diff --git a/example-react/src/app/chakra.tsx b/example-react/src/app/chakra.tsx new file mode 100644 index 0000000..d80a769 --- /dev/null +++ b/example-react/src/app/chakra.tsx @@ -0,0 +1,22 @@ +"use client"; +import { + Alert, + AlertDescription, + AlertTitle, + ChakraProvider, + defaultSystem, +} from "@chakra-ui/react"; + +export function Chakra() { + return ( + + + + Your browser! + + Your Chakra experience! + + + + ); +} diff --git a/example-react/src/app/dialog.tsx b/example-react/src/app/dialog.tsx new file mode 100644 index 0000000..664fd56 --- /dev/null +++ b/example-react/src/app/dialog.tsx @@ -0,0 +1,65 @@ +'use client' +import { Cross2Icon } from "@radix-ui/react-icons"; +import { Dialog } from "radix-ui"; + +export const DialogDemo = () => ( + + + + + + + + + Edit profile + + + Make changes to your profile here. Click save when you're done. + +
      + + +
      +
      + + +
      +
      + + + +
      + + + +
      +
      +
      +); diff --git a/example-react/src/app/select.tsx b/example-react/src/app/select.tsx new file mode 100644 index 0000000..0c578bc --- /dev/null +++ b/example-react/src/app/select.tsx @@ -0,0 +1,15 @@ +import Select from "react-select"; + +const options = [ + { value: "chocolate", label: "Chocolate" }, + { value: "strawberry", label: "Strawberry" }, + { value: "vanilla", label: "Vanilla" }, +]; + +export function WithSelect() { + return ( +
      + + + + ); +} \ No newline at end of file diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 91782e9..cce3608 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -1,4 +1,5 @@ -import { Suspense } from "react"; +import { Suspense, useActionState } from "react"; + import { Spiceflow } from "spiceflow"; import { IndexPage } from "./app/index"; import { Layout } from "./app/layout"; @@ -10,6 +11,7 @@ import { redirect, sleep } from "spiceflow/dist/utils"; import { Chakra } from "./app/chakra"; import { ClientComponentThrows, + ClientFormWithError, ErrorInUseEffect, ErrorRender, } from "./app/client"; @@ -117,6 +119,24 @@ const app = new Spiceflow()
      ); }) + .page("/form", async ({ request, children }) => { + async function action(data: FormData) { + "use server"; + console.log("action", data); + throw new Error("test error"); + return "ok"; + } + + return ( +
      + + +
      + ); + }) + .page("/form-error", async ({ request, children }) => { + return ; + }) .layout("/page/*", async ({ request, children }) => { return (
      diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef076de..fed5307 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10203,7 +10203,7 @@ snapshots: '@babel/plugin-syntax-typescript': 7.25.9(@babel/core@7.26.7) '@vanilla-extract/babel-plugin-debug-ids': 1.2.0 '@vanilla-extract/css': 1.17.1(babel-plugin-macros@3.1.0) - esbuild: 0.17.19 + esbuild: 0.17.6 eval: 0.1.8 find-up: 5.0.0 javascript-stringify: 2.1.0 diff --git a/spiceflow/src/react/components.tsx b/spiceflow/src/react/components.tsx index 5662250..651cf9a 100644 --- a/spiceflow/src/react/components.tsx +++ b/spiceflow/src/react/components.tsx @@ -36,7 +36,7 @@ export type FlightData = { // segments: MatchSegment[] page: any layouts: { id: string; element: React.ReactNode }[] - url: string + // url: string } export type ActionResult = { diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index f1866f2..ddae61d 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -29,9 +29,13 @@ async function main() { clientReferenceManifest, { callServer }, ) - // console.log({ 'action payload': payload }) - setPayload(payloadPromise) + let payload = await payloadPromise + + if (payload.actionError) { + throw payload.actionError + } + setPayload(payloadPromise) return payload.returnValue } Object.assign(globalThis, { __callServer: callServer }) diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index 62a808a..de974a2 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -899,41 +899,46 @@ export class Spiceflow< .filter(isTruthy) let root: FlightData = { - url: request.url, + // url: request.url, page, layouts, } + let actionError: Error | undefined let returnValue: unknown | undefined let formState: ReactFormState | undefined if (request.method === 'POST') { - const url = new URL(request.url) - const actionId = url.searchParams.get('__rsc') - if (actionId) { - // client stream request - const contentType = request.headers.get('content-type') - const body = contentType?.startsWith('multipart/form-data') - ? await request.formData() - : await request.text() - const args = await ReactServer.decodeReply(body) - const reference = - serverReferenceManifest.resolveServerReference(actionId) - await reference.preload() - const action = await reference.get() - // TODO handle action errors, redirects, etc - returnValue = await (action as any).apply(null, args) - } else { - // progressive enhancement - const formData = await request.formData() - console.log(formData) - const decodedAction = await ReactServer.decodeAction( - formData, - serverReferenceManifest, - ) - formState = await ReactServer.decodeFormState( - await decodedAction(), - formData, - serverReferenceManifest, - ) + try { + const url = new URL(request.url) + const actionId = url.searchParams.get('__rsc') + if (actionId) { + // client stream request + const contentType = request.headers.get('content-type') + const body = contentType?.startsWith('multipart/form-data') + ? await request.formData() + : await request.text() + const args = await ReactServer.decodeReply(body) + const reference = + serverReferenceManifest.resolveServerReference(actionId) + await reference.preload() + const action = await reference.get() + // TODO handle action errors, redirects, etc + returnValue = await (action as any).apply(null, args) + } else { + // progressive enhancement + const formData = await request.formData() + const decodedAction = await ReactServer.decodeAction( + formData, + serverReferenceManifest, + ) + formState = await ReactServer.decodeFormState( + await decodedAction(), + formData, + serverReferenceManifest, + ) + } + } catch (e) { + console.log('action error', e) + actionError = e } } @@ -947,7 +952,8 @@ export class Spiceflow< root, returnValue, formState, - }, + actionError, + } satisfies ServerPayload, clientReferenceMetadataManifest, { onPostpone(reason) { @@ -999,7 +1005,8 @@ export class Spiceflow< }, returnValue, formState, - }, + actionError, + } satisfies ServerPayload, clientReferenceMetadataManifest, ) const htmlStream = Readable.toWeb( @@ -1838,4 +1845,5 @@ export interface ServerPayload { root: FlightData formState?: ReactFormState returnValue?: unknown + actionError?: Error } From 39b9849f63024dd7fecd9c79db4d7299fa68e9fc Mon Sep 17 00:00:00 2001 From: remorses Date: Fri, 14 Feb 2025 15:35:21 +0100 Subject: [PATCH 082/100] testing form actions --- example-react/src/app/client.tsx | 32 ++++++++++++++------------- example-react/src/app/form-action.tsx | 19 ++++++++++++++++ example-react/src/app/layout.tsx | 3 ++- example-react/src/main.tsx | 24 ++++++++++++++++---- spiceflow/src/react/entry.client.tsx | 4 +++- spiceflow/src/spiceflow.tsx | 1 + 6 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 example-react/src/app/form-action.tsx diff --git a/example-react/src/app/client.tsx b/example-react/src/app/client.tsx index b4d9d4b..4d1270b 100644 --- a/example-react/src/app/client.tsx +++ b/example-react/src/app/client.tsx @@ -3,12 +3,14 @@ import "./client.css"; import React, { useActionState } from "react"; import { add } from "./action-by-client"; +import { redirect } from "spiceflow/dist/utils"; +import { action } from "./form-action"; -export function Counter() { +export function Counter({ name = "Client" }) { const [count, setCount] = React.useState(0); return (
      -
      Client counter: {count}
      +
      {name} counter: {count}
      @@ -80,22 +82,22 @@ export function ErrorRender({ error }) { console.log("caught error", error); return
      Error from rsc
      ; } - - -export function ClientFormWithError() { - async function action(_, data: FormData) { - "use server"; - console.log("action", data); - throw new Error("test error"); - return "ok"; - } - - const [state, formAction] = useActionState(action, null); +export function ClientFormWithError({ + shouldRedirect = false, + shouldError = false, + action: _action = undefined as Function, +}) { + const [state, formAction] = useActionState(_action || action, { + shouldRedirect, + shouldError, + result: "", + }); return ( -
      + +
      {JSON.stringify(state)}
      ); -} \ No newline at end of file +} diff --git a/example-react/src/app/form-action.tsx b/example-react/src/app/form-action.tsx new file mode 100644 index 0000000..e23f694 --- /dev/null +++ b/example-react/src/app/form-action.tsx @@ -0,0 +1,19 @@ +"use server"; + +import { redirect } from "spiceflow/dist/utils"; + +export async function action( + { shouldRedirect, shouldError, result }, + data: FormData, +) { + "use server"; + + console.log("action", data); + if (shouldRedirect) { + throw redirect("/"); + } + if (shouldError) { + throw new Error("test error"); + } + return { shouldRedirect, shouldError, result: "ok" }; +} diff --git a/example-react/src/app/layout.tsx b/example-react/src/app/layout.tsx index 63211c6..5b92c65 100644 --- a/example-react/src/app/layout.tsx +++ b/example-react/src/app/layout.tsx @@ -1,5 +1,6 @@ import { Link } from "spiceflow/dist/react/components"; import { ProgressBar } from "spiceflow/dist/react/progress"; +import { Counter } from "./client"; export function Layout(props: React.PropsWithChildren) { return ( @@ -13,9 +14,9 @@ export function Layout(props: React.PropsWithChildren) { +
      • - Home
      • diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index cce3608..59f9327 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -119,12 +119,14 @@ const app = new Spiceflow()
      ); }) - .page("/form", async ({ request, children }) => { + .page("/form-server", async ({ state, children }) => { async function action(data: FormData) { "use server"; - console.log("action", data); - throw new Error("test error"); - return "ok"; + if (!state) { + throw new Error("userId not set"); + } + + return } return ( @@ -135,8 +137,22 @@ const app = new Spiceflow() ); }) .page("/form-error", async ({ request, children }) => { + return ; + }) + .page("/form", async ({ request, children }) => { return ; }) + .page("/form-redirect", async ({ request, children }) => { + return ; + }) + .page("/form-inline-action-server", async ({ state, children }) => { + async function action({}) { + "use server"; + console.log({ state }); + return { state, hello: true }; + } + return ; + }) .layout("/page/*", async ({ request, children }) => { return (
      diff --git a/spiceflow/src/react/entry.client.tsx b/spiceflow/src/react/entry.client.tsx index ddae61d..d50f2ed 100644 --- a/spiceflow/src/react/entry.client.tsx +++ b/spiceflow/src/react/entry.client.tsx @@ -16,6 +16,7 @@ import { } from './components.js' import { ServerPayload } from '../spiceflow.js' import { FlightDataContext } from './context.js' +import { createError, getErrorContext } from './errors.js' async function main() { const callServer: CallServerFn = async (id, args) => { @@ -31,8 +32,9 @@ async function main() { ) let payload = await payloadPromise - + if (payload.actionError) { + console.log(getErrorContext(payload.actionError)) throw payload.actionError } setPayload(payloadPromise) diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index de974a2..e8087d5 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -43,6 +43,7 @@ import { LayoutContent, } from './react/components.js' import { + createError, getErrorContext, isNotFoundError, isRedirectError, From 1536fa3fc64d8d2adaad83bfb9cc4d877e737091 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 17 Feb 2025 15:30:39 +0100 Subject: [PATCH 083/100] handle errors in passthrough --- spiceflow/src/spiceflow.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/spiceflow/src/spiceflow.tsx b/spiceflow/src/spiceflow.tsx index e8087d5..7f1c5fc 100644 --- a/spiceflow/src/spiceflow.tsx +++ b/spiceflow/src/spiceflow.tsx @@ -976,10 +976,19 @@ export class Spiceflow< const nodeStream = abortable.pipe(passthrough) const stream = Readable.toWeb(nodeStream) as ReadableStream const start = performance.now() - await new Promise((resolve) => { + await new Promise((resolve, reject) => { passthrough.once('data', () => { resolve() }) + passthrough.once('error', (err) => { + reject(err) + }) + passthrough.once('end', () => { + resolve() // Resolve if stream ends before data + }) + passthrough.once('close', () => { + resolve() // Resolve if stream closes + }) }) const end = performance.now() // console.log(`First chunk took ${Math.round(end - start)}ms`) From cc1259ac0d0095dcdaea2e40ea81a4fcce57bd15 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 17 Feb 2025 15:32:57 +0100 Subject: [PATCH 084/100] handle error in ssr entry --- spiceflow/src/react/entry.ssr.tsx | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/spiceflow/src/react/entry.ssr.tsx b/spiceflow/src/react/entry.ssr.tsx index 0dfeb8d..367a422 100644 --- a/spiceflow/src/react/entry.ssr.tsx +++ b/spiceflow/src/react/entry.ssr.tsx @@ -30,21 +30,26 @@ export default async function handler( } export async function fetchHandler(request: Request) { - const url = new URL(request.url) - const rscEntry = await importRscEntry() - const response = await rscEntry.handler(request) + try { + const url = new URL(request.url) + const rscEntry = await importRscEntry() + const response = await rscEntry.handler(request) - if (!response.headers.get('content-type')?.startsWith('text/x-component')) { - return response - } + if (!response.headers.get('content-type')?.startsWith('text/x-component')) { + return response + } - if (url.searchParams.has('__rsc')) { - return response - } + if (url.searchParams.has('__rsc')) { + return response + } - const htmlResponse = await renderHtml({ response, request }) + const htmlResponse = await renderHtml({ response, request }) - return htmlResponse + return htmlResponse + } catch (err) { + console.error('[fetchHandler] unexpected error', err) + return new Response('', { status: 500 }) + } } async function renderHtml({ From ae43a13228dc4ff1cc1ba96fa60c3991f0c794d7 Mon Sep 17 00:00:00 2001 From: remorses Date: Mon, 17 Feb 2025 15:54:57 +0100 Subject: [PATCH 085/100] made a prerelease --- .changeset/config.json | 14 +++++++------- .changeset/five-toes-learn.md | 5 +++++ .changeset/pre.json | 12 ++++++++++++ example-react/package.json | 2 +- how-is-this-not-illegal/package.json | 1 - spiceflow/CHANGELOG.md | 24 ++++++++++++++++++++++++ spiceflow/package.json | 4 ++-- 7 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 .changeset/five-toes-learn.md create mode 100644 .changeset/pre.json diff --git a/.changeset/config.json b/.changeset/config.json index 8e872d9..b7cbab7 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,9 +1,9 @@ { - "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "linked": [], - "access": "public", - "baseBranch": "main", - "updateInternalDependencies": "patch" + "$schema": "https://unpkg.com/@changesets/config@1.4.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch" } diff --git a/.changeset/five-toes-learn.md b/.changeset/five-toes-learn.md new file mode 100644 index 0000000..38b546f --- /dev/null +++ b/.changeset/five-toes-learn.md @@ -0,0 +1,5 @@ +--- +'spiceflow': patch +--- + +initial rsc release diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000..79b4c80 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,12 @@ +{ + "mode": "pre", + "tag": "rsc", + "initialVersions": { + "openapi-schema-diff": "0.0.1", + "spiceflow": "1.6.1", + "how-is-this-not-illegal": "0.1.0" + }, + "changesets": [ + "five-toes-learn" + ] +} diff --git a/example-react/package.json b/example-react/package.json index 5853489..a6f7f2d 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -1,8 +1,8 @@ { "name": "example-react", - "version": "1.0.0", "description": "", "main": "index.js", + "private": true, "type": "module", "scripts": { "dev": "DEBUG_SPICEFLOW=1 vite", diff --git a/how-is-this-not-illegal/package.json b/how-is-this-not-illegal/package.json index aaa33d8..62a38f7 100644 --- a/how-is-this-not-illegal/package.json +++ b/how-is-this-not-illegal/package.json @@ -1,6 +1,5 @@ { "name": "how-is-this-not-illegal", - "version": "0.1.0", "private": true, "type": "module", "scripts": { diff --git a/spiceflow/CHANGELOG.md b/spiceflow/CHANGELOG.md index b167059..901856f 100644 --- a/spiceflow/CHANGELOG.md +++ b/spiceflow/CHANGELOG.md @@ -1,5 +1,29 @@ # spiceflow +## 1.6.2-rsc.2 + +### Patch Changes + +- initial rsc release +- Updated dependencies + - spiceflow@1.6.2-rsc.2 + +## 1.6.2-rsc.1 + +### Patch Changes + +- initial rsc release +- Updated dependencies + - spiceflow@1.6.2-rsc.1 + +## 1.6.2-rsc.0 + +### Patch Changes + +- initial rsc release +- Updated dependencies + - spiceflow@1.6.2-rsc.0 + ## 1.6.1 ### Patch Changes diff --git a/spiceflow/package.json b/spiceflow/package.json index 8c7d2c4..b480344 100644 --- a/spiceflow/package.json +++ b/spiceflow/package.json @@ -1,6 +1,6 @@ { "name": "spiceflow", - "version": "1.6.1", + "version": "1.6.2-rsc.2", "description": "Simple API framework with RPC and type safety", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -75,7 +75,7 @@ "react-dom": "19.0.0", "rsc-html-stream": "^0.0.4", "sirv": "^3.0.0", - "spiceflow": "*", + "spiceflow": "1.6.2-rsc.2", "superjson": "^2.2.2", "unplugin-rsc": "^0.0.11", "vite": "^6.1.0", From a64a400c961108c5eda4e744e07ec7af16990b47 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Sun, 1 Mar 2026 18:25:50 +0100 Subject: [PATCH 086/100] migrate RSC from custom implementation to @vitejs/plugin-rsc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the custom RSC setup (@jacob-ebey/react-server-dom-vite + unplugin-rsc + hand-rolled reference tracking) with the official @vitejs/plugin-rsc@0.5.21. ## What changed **Dependencies:** - Remove @jacob-ebey/react-server-dom-vite, unplugin-rsc, @hiogawa/transforms, @hiogawa/utils - Add @vitejs/plugin-rsc@0.5.21, react-server-dom-webpack@19.2.4 - Upgrade react/react-dom to 19.2.4 **vite.tsx — completely rewritten:** - Replace ~600 lines of custom RSC plugin code (manual client/server reference tracking, custom environment configs, SSE-based HMR, manual module graph walking) with a thin wrapper around `rsc()` from @vitejs/plugin-rsc - Add `spiceflow:optimize-deps-rewrite` plugin to rewrite optimizeDeps entries with `spiceflow >` prefix so vendor CJS files resolve through the framework package (where plugin-rsc is installed) rather than from the app root - Add `spiceflow:auto-use-client` plugin for client-by-default behavior — auto injects `"use client"` into user source files unless they already have a directive, are the app entry, node_modules, spiceflow internals, or .server.* **Entry points updated to use plugin-rsc APIs:** - entry.client.tsx: import from `@vitejs/plugin-rsc/browser` instead of custom references.browser + react-server-dom-vite/client - entry.ssr.tsx: import `createFromReadableStream` from `@vitejs/plugin-rsc/ssr`, use `import.meta.viteRsc.loadModule` and `import.meta.viteRsc.loadBootstrapScriptContent` - entry.rsc.tsx: simplified — just imports app entry and exports handler **spiceflow.tsx — renderReact updated:** - Import renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction from `@vitejs/plugin-rsc/rsc` instead of custom wrappers - Remove manual client reference manifest handling **Deleted files (old custom RSC infrastructure):** - references.browser.tsx, references.rsc.tsx, references.ssr.tsx - utils/client-reference.ts, utils/normalize.ts - react/types/index.ts **Example app fixes for stricter RSC semantics:** - action.tsx: make getCounter async (plugin-rsc rejects non-async exports in "use server" modules) - index.tsx: receive counter + serverRandom as props instead of calling server functions during render (client components can't call server functions during SSR initial render) - main.tsx: fetch data in page handlers and pass as props — correct RSC pattern - e2e tests: fix selectors for dual counter elements, replace `using` keyword with try/finally for Playwright compat ## Test results - 12/12 Playwright e2e tests pass (dev mode) - 216/216 spiceflow package tests pass - Production build: 4/5 stages succeed, prerender step has a timing issue with __vite_rsc_assets_manifest.js generation (TODO) --- .gitignore | 4 +- example-react/e2e/basic.test.ts | 25 +- example-react/package.json | 4 +- example-react/src/app/action.tsx | 2 +- example-react/src/app/index.tsx | 8 +- example-react/src/main.tsx | 21 +- pnpm-lock.yaml | 1847 ++++++++++------- spiceflow/package.json | 10 +- spiceflow/src/react/entry.client.tsx | 54 +- spiceflow/src/react/entry.rsc.tsx | 6 +- spiceflow/src/react/entry.ssr.tsx | 50 +- spiceflow/src/react/references.browser.tsx | 5 - spiceflow/src/react/references.rsc.tsx | 5 - spiceflow/src/react/references.ssr.tsx | 4 - spiceflow/src/react/types/ambient.d.ts | 84 +- spiceflow/src/react/types/index.ts | 18 - spiceflow/src/react/utils/client-reference.ts | 37 - spiceflow/src/react/utils/normalize.ts | 34 - spiceflow/src/spiceflow.tsx | 151 +- spiceflow/src/vite.tsx | 702 ++----- 20 files changed, 1341 insertions(+), 1730 deletions(-) delete mode 100644 spiceflow/src/react/references.browser.tsx delete mode 100644 spiceflow/src/react/references.rsc.tsx delete mode 100644 spiceflow/src/react/references.ssr.tsx delete mode 100644 spiceflow/src/react/utils/client-reference.ts delete mode 100644 spiceflow/src/react/utils/normalize.ts diff --git a/.gitignore b/.gitignore index f5767e7..ce6f4b5 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ __pycache__ debug .env .last-run.json -.react-router \ No newline at end of file +.react-router +opensrc +/di diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index cc6c825..4414a5a 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -55,14 +55,12 @@ test.describe("redirect", () => { test("client reference", async ({ page }) => { await page.goto("/"); await page.getByText("[hydrated: 1]").click(); - await page.getByText("Client counter: 0").click(); - await page - .getByTestId("client-counter") - .getByRole("button", { name: "+" }) - .click(); - await page.getByText("Client counter: 1").click(); + const clientCounter = page.getByTestId("client-counter").filter({ hasText: "Client counter" }); + await clientCounter.getByText("Client counter: 0").click(); + await clientCounter.getByRole("button", { name: "+" }).click(); + await clientCounter.getByText("Client counter: 1").click(); await page.reload(); - await page.getByText("Client counter: 0").click(); + await clientCounter.getByText("Client counter: 0").click(); }); test("server reference in server @js", async ({ page }) => { @@ -139,9 +137,13 @@ test("client hmr @dev", async ({ page }) => { .click(); await page.getByText("Client counter: 1").click(); // edit client - using file = createEditor("src/app/client.tsx"); - file.edit((s) => s.replace("Client counter", "Client [EDIT] counter")); - await page.getByText("Client [EDIT] counter: 1").click(); + const file = createEditor("src/app/client.tsx"); + try { + file.edit((s) => s.replace("Client counter", "Client [EDIT] counter")); + await page.getByText("Client [EDIT] counter: 1").click(); + } finally { + file[Symbol.dispose](); + } }); test("server hmr @dev", async ({ page }) => { @@ -165,7 +167,7 @@ test("server hmr @dev", async ({ page }) => { await page.getByText("Client counter: 1").click(); // edit server - using file = createEditor("src/app/index.tsx"); + const file = createEditor("src/app/index.tsx"); await file.edit((s) => s.replace("Server counter", "Server [EDIT] counter")); await page.getByText("Server [EDIT] counter: 1").click(); await page.getByText("Client counter: 1").click(); @@ -176,4 +178,5 @@ test("server hmr @dev", async ({ page }) => { .getByRole("button", { name: "-" }) .click(); await page.getByText("Server [EDIT] counter: 0").click(); + file[Symbol.dispose](); }); diff --git a/example-react/package.json b/example-react/package.json index a6f7f2d..b663968 100644 --- a/example-react/package.json +++ b/example-react/package.json @@ -21,8 +21,8 @@ "@types/react": "^19.0.8", "@types/react-dom": "^19.0.3", "radix-ui": "^1.1.3", - "react": "19.0.0", - "react-dom": "19.0.0", + "react": "19.2.4", + "react-dom": "19.2.4", "react-select": "^5.10.0", "spiceflow": "workspace:*", "tailwindcss": "^4.0.5", diff --git a/example-react/src/app/action.tsx b/example-react/src/app/action.tsx index f1f77a6..5228ee6 100644 --- a/example-react/src/app/action.tsx +++ b/example-react/src/app/action.tsx @@ -4,7 +4,7 @@ if (!("counter" in globalThis)) { globalThis.counter = 0; } -export function getCounter() { +export async function getCounter() { return globalThis.counter; } diff --git a/example-react/src/app/index.tsx b/example-react/src/app/index.tsx index f7bb019..e2e0cb6 100644 --- a/example-react/src/app/index.tsx +++ b/example-react/src/app/index.tsx @@ -1,11 +1,11 @@ import { Button } from "./button"; -import { changeCounter, getCounter } from "./action"; +import { changeCounter } from "./action"; import { Calculator, Counter, Hydrated } from "./client"; -export async function IndexPage() { +export function IndexPage({ counter, serverRandom }: { counter: number; serverRandom: string }) { return (
      -
      server random: {Math.random().toString(36).slice(2)}
      +
      server random: {serverRandom}
      -
      Server counter: {getCounter()}
      +
      Server counter: {counter}
      Unicode test: 🌟 你好 こんにちは ⚡️ 안녕하세요
      +
      + ) +} +``` + +### Server Actions + +Use `"use server"` to define functions that run on the server but can be called from client components (e.g. form actions). + +```tsx +// src/app/actions.tsx +'use server' + +export async function submitForm(formData: FormData) { + const name = formData.get('name') + await saveToDatabase(name) +} +``` + +### Client Code Splitting + +Code splitting of client components is **automatic** — you don't need `React.lazy()` or dynamic `import()`. Each `"use client"` file becomes a separate chunk, and the browser only loads the chunks needed for the current page. + +**How it works:** when the RSC flight stream is sent to the browser, it contains references to client component chunks rather than the actual code. The browser resolves and loads only the chunks referenced on the current page. If route `/about` uses `` and route `/dashboard` uses ``, visiting `/about` will never download the Chart component's JavaScript. + +**Avoid barrel files with `"use client"`.** If you have a single file with `"use client"` that re-exports many components, all of them end up in one chunk — defeating code splitting. Instead, put `"use client"` in each individual component file: + +```tsx +// BAD — one big chunk for everything +// src/components/index.tsx +'use client' +export { Chart } from './chart' +export { Map } from './map' +export { Table } from './table' +``` + +```tsx +// GOOD — each component is its own chunk +// src/components/chart.tsx +'use client' +export function Chart() { /* ... */ } + +// src/components/map.tsx +'use client' +export function Map() { /* ... */ } + +// Re-export barrel has no directive, just passes through +// src/components/index.tsx +export { Chart } from './chart' +export { Map } from './map' +``` From a86467a3281de3c91bc5b48bf70cfca4139ce68e Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Mon, 2 Mar 2026 16:42:34 +0100 Subject: [PATCH 099/100] test: add serverRenderCount counter and verify client HMR doesn't trigger server re-render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a server-side render counter (serverRenderCount) that increments on each RSC render of the home page, exposed via data-testid="server-render-count". The client HMR test now asserts that: - Editing a client component does NOT increment the server render count - Client state is preserved (counter stays at 1 after edit) - The edited text appears via React Fast Refresh This proves client component edits only trigger client-side HMR, not a server re-render. Vite's SSR environment logs 'page reload' internally but the browser does not actually reload — React Fast Refresh handles the update. Also update AGENTS.md e2e testing section with: - Accurate HMR behavior (client HMR preserves state, no server re-render) - Warning about replace() no-ops when the search string doesn't exist in source - Documentation for the serverRenderCount test helper --- AGENTS.md | 72 +++++++++++++++++++++++++++++++++ example-react/e2e/basic.test.ts | 27 ++++++++----- example-react/src/main.tsx | 6 +++ 3 files changed, 94 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9a3f7d2..5ec4cf3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,6 +30,78 @@ Sometimes tests work directly on database data, using prisma. To run these tests Never write tests yourself that call prisma or interact with database or emails. For these asks the user to write them for you. +# e2e testing (example-react) + +E2e tests live in `example-react/e2e/` and use Playwright (chromium only). The dev server starts automatically via the `webServer` config in `playwright.config.ts`. + +## running e2e tests + +```bash +# run from example-react directory, never from root +cd example-react + +# run all e2e tests +pnpm test-e2e + +# filter by test name +pnpm test-e2e --grep "SSR error" + +# run against production build +pnpm test-e2e-preview +``` + +Tests tagged `@dev` are skipped during preview runs; tests tagged `@build` are skipped during dev runs (controlled by `grepInvert` in playwright.config.ts). + +## rebuild dist before testing + +The Vite SSR middleware imports from `spiceflow/dist/` (the compiled package), NOT from source. If you modify files in `spiceflow/src/`, you must rebuild before e2e tests will pick up the changes: + +```bash +cd spiceflow +pnpm tsc --noCheck # --noCheck skips pre-existing type errors +``` + +This is the most common reason e2e tests fail after code changes — stale dist files. + +## writing e2e tests + +- The base URL and port are defined at the top of `basic.test.ts`: + ```ts + const port = Number(process.env.E2E_PORT || 6174); + const baseURL = `http://localhost:${port}`; + ``` +- Use `page.goto("/path")` for browser-based tests that need rendering, JS execution, or DOM interaction. +- Use Node.js `fetch(baseURL + "/path")` directly (not `page.evaluate`) when you need to control HTTP headers like `Origin` — browsers restrict forbidden headers. +- Use `page.getByTestId()`, `page.getByText()`, `page.getByRole()` for locators. Prefer test-ids for stability. +- When a `data-testid` matches multiple elements (e.g. multiple counter components on a page), use `.filter({ hasText: "..." })` to disambiguate: + ```ts + const clientCounter = page.getByTestId("client-counter").filter({ hasText: "Client counter" }); + await clientCounter.getByRole("button", { name: "+" }).click(); + ``` +- If a locator's text changes during the test (e.g. HMR edits), do NOT use it through a pre-filtered variable — query the page directly for the new text. + +## adding test routes + +To add a route for e2e testing, add it in `example-react/src/main.tsx` using the spiceflow API: + +```ts +.page("/my-test-route", async () => { + return ; +}) +``` + +Client components used in tests should be created in `example-react/src/app/` with a `"use client"` directive. + +## HMR tests + +- `createEditor("src/app/file.tsx")` from `e2e/helper.ts` edits a file and auto-reverts on dispose. +- Always call `file[Symbol.dispose]()` or use `try/finally` to restore files after edits. +- When editing files, make sure the `replace()` string actually exists in the source. For example, `client.tsx` has `name = "Client"` as a default prop — the literal string "Client counter" does NOT exist in the file, so `replace("Client counter", ...)` would be a no-op and the HMR test would silently fail. +- **Client HMR preserves state**: editing a client component triggers React Fast Refresh without a server re-render. Client state is preserved. Vite's SSR environment logs `page reload` internally but the browser does not actually reload — Fast Refresh handles it. +- **Server HMR preserves server state**: editing a server component triggers RSC HMR. Server-side state (e.g. counters stored in module scope) is preserved. Client state is also preserved because no full page reload occurs. +- The home page has a `serverRenderCount` counter (`data-testid="server-render-count"`) that increments on each RSC render. Use it in tests to verify whether a server re-render happened. + + # website the website uses react-router v7. diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index bda7746..0ab66be 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -129,24 +129,29 @@ async function testServerAction2(page: Page, options: { js: boolean }) { } } -// RSC architecture causes SSR page reload on client component changes, which -// races with client-side HMR and prevents the edited text from appearing reliably. -test.skip("client hmr @dev", async ({ page }) => { +test("client hmr @dev", async ({ page }) => { await page.goto("/"); await page.getByText("[hydrated: 1]").click(); const clientCounter = page.getByTestId("client-counter").filter({ hasText: "Client counter" }); // client +1 - await clientCounter.getByText("Client counter: 0").click(); - await clientCounter - .getByRole("button", { name: "+" }) - .click(); + await clientCounter.getByRole("button", { name: "+" }).click(); await clientCounter.getByText("Client counter: 1").click(); - // edit client — RSC architecture causes a full page reload (SSR re-renders client components), - // so client state resets to 0. We verify the edited text appears, not that state is preserved. + // Record the server render count before the client edit + const renderCountBefore = await page.getByTestId("server-render-count").textContent(); + // edit client — replace the default prop value in client.tsx. + // Client HMR should NOT trigger a server re-render. Only the client module + // should hot-update, preserving client state and avoiding an SSR page reload. const file = createEditor("src/app/client.tsx"); try { - file.edit((s) => s.replace("Client counter", "Client [EDIT] counter")); - await page.getByText("Client [EDIT] counter: 0").click(); + await file.edit((s) => s.replace('name = "Client"', 'name = "Client [EDIT]"')); + // Verify edited text appears with preserved state (counter stays at 1). + // If a full page reload happened, state would reset to 0. + await expect(page.getByText("Client [EDIT] counter: 1")).toBeVisible(); + // Wait to ensure any delayed server re-render would have completed + await page.waitForTimeout(2000); + // Server render count must not have changed — no server re-render happened + const renderCountAfter = await page.getByTestId("server-render-count").textContent(); + expect(renderCountAfter).toBe(renderCountBefore); } finally { file[Symbol.dispose](); } diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 6179476..4988984 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -22,6 +22,10 @@ import { ThrowsDuringSSR } from "./app/ssr-error"; import { Head } from "spiceflow/dist/react/head"; import { SpiceflowContext } from "spiceflow/dist/context"; +// Increments on every RSC render of the home page. Used by e2e tests to detect +// unwanted server re-renders (e.g. client HMR should not trigger a server render). +let serverRenderCount = 0; + const app = new Spiceflow() .state("middleware1", "") .use(async ({ request, state }, next) => { @@ -48,11 +52,13 @@ const app = new Spiceflow() ); }) .page("/", async ({ request }) => { + serverRenderCount++; const counter = await getCounter(); const serverRandom = Math.random().toString(36).slice(2); return ( <> title from page + {serverRenderCount} ); From 101c8671d3be2936073950cd467226741febe78d Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Mon, 2 Mar 2026 16:53:49 +0100 Subject: [PATCH 100/100] test: streaming async generator from server to client component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add e2e test proving that an async generator passed as a prop from a server component to a client component streams items incrementally — the client starts rendering before the generator completes. The test uses waitUntil:'commit' so Playwright begins observing while the HTML stream is still open. It asserts: - 'message-1' is visible while the 'done' marker is NOT (generator has ~3s left) - All 3 items arrive in order once the generator finishes This works because React 19's flight protocol natively serializes async iterables: the server yields values progressively via X/x flight chunks, and the client reconstructs a live AsyncIterable backed by a promise queue that resolves as chunks arrive over the wire. New files: - example-react/src/app/streaming-consumer.tsx — client component consuming AsyncIterable via useEffect + for-await - /streaming route in main.tsx with 1.5s delays between yields --- example-react/e2e/basic.test.ts | 21 +++++++++++ example-react/src/app/streaming-consumer.tsx | 39 ++++++++++++++++++++ example-react/src/main.tsx | 11 ++++++ 3 files changed, 71 insertions(+) create mode 100644 example-react/src/app/streaming-consumer.tsx diff --git a/example-react/e2e/basic.test.ts b/example-react/e2e/basic.test.ts index 0ab66be..4c6f8cd 100644 --- a/example-react/e2e/basic.test.ts +++ b/example-react/e2e/basic.test.ts @@ -244,3 +244,24 @@ test.describe("CSRF protection", () => { expect(response.status).not.toBe(403); }); }); + +test.describe("streaming async generator", () => { + test("client renders items incrementally before generator completes", async ({ page }) => { + // Use waitUntil:'commit' so Playwright doesn't wait for the full streaming response + await page.goto("/streaming", { waitUntil: "commit" }); + // First item should appear while the generator is still yielding + const firstItem = page.getByTestId("stream-item").first(); + await expect(firstItem).toBeVisible({ timeout: 10000 }); + await expect(firstItem).toHaveText("message-1"); + // At this point the generator still has ~3s of work left (2 × 1500ms delays). + // "done" marker must NOT be visible yet. + expect(await page.getByTestId("stream-done").isVisible()).toBe(false); + // Wait for all items to arrive + await expect(page.getByTestId("stream-done")).toBeVisible({ timeout: 10000 }); + const items = page.getByTestId("stream-item"); + await expect(items).toHaveCount(3); + await expect(items.nth(0)).toHaveText("message-1"); + await expect(items.nth(1)).toHaveText("message-2"); + await expect(items.nth(2)).toHaveText("message-3"); + }); +}); diff --git a/example-react/src/app/streaming-consumer.tsx b/example-react/src/app/streaming-consumer.tsx new file mode 100644 index 0000000..9270db2 --- /dev/null +++ b/example-react/src/app/streaming-consumer.tsx @@ -0,0 +1,39 @@ +// Client component that consumes an async iterable prop, rendering items +// incrementally as they arrive from the server via the RSC flight stream. +"use client"; + +import { useEffect, useState } from "react"; + +export function StreamingConsumer({ + stream, +}: { + stream: AsyncIterable; +}) { + const [items, setItems] = useState([]); + const [done, setDone] = useState(false); + + useEffect(() => { + let cancelled = false; + (async () => { + for await (const item of stream) { + if (cancelled) break; + setItems((prev) => [...prev, item]); + } + if (!cancelled) setDone(true); + })(); + return () => { + cancelled = true; + }; + }, [stream]); + + return ( +
      + {items.map((item, i) => ( +
      + {item} +
      + ))} + {done &&
      done
      } +
      + ); +} diff --git a/example-react/src/main.tsx b/example-react/src/main.tsx index 4988984..d4217a8 100644 --- a/example-react/src/main.tsx +++ b/example-react/src/main.tsx @@ -19,6 +19,7 @@ import { import { DialogDemo } from "./app/dialog"; import { WithSelect } from "./app/select"; import { ThrowsDuringSSR } from "./app/ssr-error"; +import { StreamingConsumer } from "./app/streaming-consumer"; import { Head } from "spiceflow/dist/react/head"; import { SpiceflowContext } from "spiceflow/dist/context"; @@ -217,6 +218,16 @@ const app = new Spiceflow() .page("/client-error", async () => { return ; }) + .page("/streaming", async () => { + async function* generateMessages() { + yield "message-1"; + await sleep(1500); + yield "message-2"; + await sleep(1500); + yield "message-3"; + } + return ; + }) .page("/ssr-error-fallback", async () => { return ; })