Open simonedelmann opened 4 months ago
I made some attempt to implement this functionality myself, see files below. This should be trivial to implement into Vinxi. Please let me know if I should make a PR for this.
app.config.ts
import { fileURLToPath } from 'node:url'
// @ts-expect-error - no types yet
import { serverFunctions } from '@vinxi/server-functions/plugin'
// @ts-expect-error - no types yet
import { server } from '@vinxi/server-functions/server'
import react from '@vitejs/plugin-react-swc'
import { createApp } from 'vinxi'
import { normalize } from 'vinxi/lib/path'
import tsconfigPaths from 'vite-tsconfig-paths'
export default createApp({
routers: [
{
name: 'client',
target: 'browser',
type: 'spa',
handler: 'index.html',
plugins: () => [
tsconfigPaths(),
serverFunctions.client({
runtime: normalize(
fileURLToPath(
new URL('./vinxi/client-runtime.js', import.meta.url),
),
),
}),
react(),
],
},
{
name: 'server-fns',
type: 'http',
base: '/_server',
handler: fileURLToPath(
new URL('./vinxi/server-handler.js', import.meta.url),
),
target: 'server',
plugins: async () => [server(), tsconfigPaths()],
},
],
})
vinxi/client-runtime.js
import SuperJSON from 'superjson'
async function fetchServerAction(base, id, args) {
const response = await fetch(base, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'server-action': id,
},
body: JSON.stringify(SuperJSON.serialize(args)),
})
return SuperJSON.deserialize(await response.json())
}
export function createServerReference(fn, id, name) {
return new Proxy(fn, {
get(target, prop, receiver) {
if (prop === 'url') {
return `${import.meta.env.SERVER_BASE_URL}/_serverid=${id}&name=${name}`
}
return Reflect.get(target, prop, receiver)
},
apply(target, thisArg, args) {
return fetchServerAction(
`${import.meta.env.SERVER_BASE_URL}/_server`,
`${id}#${name}`,
args,
)
},
})
}
vinxi/server-handler.js
import SuperJSON from 'superjson'
/// <reference types="vinxi/types/server" />
import { eventHandler, getHeader, readBody, setHeader } from 'vinxi/http'
import invariant from 'vinxi/lib/invariant'
import { getManifest } from 'vinxi/manifest'
export async function handleServerAction(event) {
invariant(event.method === 'POST', 'Invalid method')
const serverReference = getHeader(event, 'server-action')
if (serverReference) {
invariant(typeof serverReference === 'string', 'Invalid server action')
// This is the client-side case
const [filepath, name] = serverReference.split('#')
const action = (
await getManifest(import.meta.env.ROUTER_NAME).chunks[filepath].import()
)[name]
const json = SuperJSON.deserialize(await readBody(event))
const result = action.apply(null, json)
try {
// Wait for any mutations
const response = await result
setHeader(event, 'Content-Type', 'application/json')
setHeader(event, 'Router', 'server-fns')
return JSON.stringify(SuperJSON.serialize(response ?? null))
} catch (x) {
console.error(x)
return new Response(
JSON.stringify(SuperJSON.serialize({ error: x.message })),
{
status: 500,
headers: {
'Content-Type': 'application/json',
'x-server-function': 'error',
},
},
)
}
} else {
throw new Error('Invalid request')
}
}
export default eventHandler(handleServerAction)
One main benefit of server functions (at least for me) is, that server and client share the same types. This works well as long as the payload is encodable as JSON. But Dates/Sets/Maps/… will result in unexpected surprises.
tRPC solves this by adding a Data Transformer, which enhances
JSON.stringify()
andJSON.parse()
/ replaces it by SuperJSON. (https://trpc.io/docs/server/data-transformers)It would be great to include such a feature to Vinxi‘s server functions, too.