Got working preact SSR

pull/1/head
Antonio De Lucreziis 2 years ago
parent 5142d4c551
commit ddad7fe746

@ -1 +1,2 @@
auto-install-peers=true # Needed by pnpm to work with "@preact/preset-vite"
shamefully-hoist=true

@ -1,13 +1,30 @@
import Router from 'preact-router' import Router from 'preact-router'
import { route } from 'preact-router'
import { useEffect } from 'preact/hooks'
import { HomePage } from './pages/Home.jsx' import { HomePage } from './pages/Home.jsx'
import { LoginPage } from './pages/Login.jsx' import { LoginPage } from './pages/Login.jsx'
import { ProblemPage } from './pages/Problem.jsx' import { ProblemPage } from './pages/Problem.jsx'
export const App = ({ path }) => ( const Redirect = ({ to }) => {
<Router> useEffect(() => {
route(to, true)
}, [])
return (
<>
Redirecting to <pre>{to}</pre>...
</>
)
}
export const App = ({ url }) => {
return (
<Router url={url}>
<HomePage path="/" /> <HomePage path="/" />
<LoginPage path="/login" /> <LoginPage path="/login" />
<ProblemPage path="/problem/:id" /> <ProblemPage path="/problem/:id" />
<Redirect default to="/" />
</Router> </Router>
) )
}

@ -1,79 +0,0 @@
import { useSignal } from '@preact/signals'
import { useEffect, useMemo } from 'preact/hooks'
import URLPattern from 'url-pattern'
export const Router = ({ pages }) => {
const compiledRoutes = useMemo(
() =>
Object.entries(pages).map(([pattern, Page]) => ({
pattern: new URLPattern(pattern),
Page,
})),
[pages]
)
const routerUrl = useSignal(location.hash.slice(1))
useEffect(() => {
window.addEventListener('hashchange', () => {
routerUrl.value = location.hash.slice(1)
})
}, [])
const route = compiledRoutes.flatMap(({ pattern, Page }) => {
const m = pattern.match(routerUrl.value.split('?', 1)[0])
return m ? [{ Page, params: m }] : []
})?.[0]
if (!route) {
console.log(`Invalid route "${routerUrl.value}", redirecting to homepage`)
location.href = '/#/'
routerUrl.value = '/'
return <>Redirecting...</>
}
const { Page, params } = route
const queryPart =
routerUrl.value.indexOf('?') === -1
? ''
: routerUrl.value.slice(routerUrl.value.indexOf('?') + 1)
console.log(queryPart)
const queryParams =
queryPart?.length > 0
? Object.fromEntries(
queryPart.split('&').map(kvPart => {
const eqIndex = kvPart.indexOf('=')
return eqIndex === -1
? [kvPart, true]
: [
kvPart.slice(0, eqIndex),
decodeURIComponent(kvPart.slice(eqIndex + 1)),
]
})
)
: {}
return <Page params={params} query={queryParams} />
}
export const Link = ({ page, params, query, children }) => {
for (const [key, value] of Object.entries(params ?? {})) {
page = page.replace(':' + key, encodeURIComponent(value))
}
let targetHref = page
if (query) {
targetHref +=
'?' +
Object.entries(query ?? {})
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&')
}
return <a href={'/#' + targetHref}>{children}</a>
}

@ -1,4 +1,3 @@
import { Link } from '../Router.jsx'
import { Markdown } from './Markdown.jsx' import { Markdown } from './Markdown.jsx'
export const Problem = ({ id, content }) => { export const Problem = ({ id, content }) => {
@ -6,9 +5,7 @@ export const Problem = ({ id, content }) => {
<div class="problem"> <div class="problem">
<div class="problem-header"> <div class="problem-header">
<div class="problem-title"> <div class="problem-title">
<Link page={`/problem/:id`} params={{ id }}> <a href={`/problem/${id}`}>Problema {id}</a>
Problema {id}
</Link>
</div> </div>
</div> </div>
<div class="problem-content"> <div class="problem-content">

@ -0,0 +1,4 @@
import { hydrate } from 'preact'
import { App } from './App.jsx'
hydrate(<App />, document.body)

@ -0,0 +1,6 @@
import renderToString from 'preact-render-to-string'
import { App } from './App.jsx'
export function render(url) {
return renderToString(<App url={url} />)
}

@ -1,15 +1,24 @@
import { useEffect, useState } from 'preact/hooks' import { useEffect, useState } from 'preact/hooks'
export const useUser = () => { export const useUser = () => {
const [username, setUsername] = useState(null) const [user, setUser] = useState(null)
const logout = () => {
setUser(null)
}
useEffect(async () => { useEffect(async () => {
const res = await fetch(`/api/current-user`, { const res = await fetch(`/api/current-user`, {
credentials: 'include', credentials: 'include',
}) })
const username = await res.json()
setUsername(username) if (res.ok) {
const user = await res.json()
setUser(user)
}
}, []) }, [])
return { username } console.log(user)
return [user, logout]
} }

@ -1,15 +1,19 @@
import { route } from 'preact-router'
import { Problem } from '../components/Problem.jsx' import { Problem } from '../components/Problem.jsx'
import { useUser } from '../hooks.jsx' import { useUser } from '../hooks.jsx'
export const HomePage = () => { export const HomePage = () => {
const { username } = useUser() console.log('rendering homepage')
const [user, logout] = useUser()
const logout = async () => { const handleLogout = async () => {
await fetch(`/api/logout`, { await fetch(`/api/logout`, {
method: 'POST', method: 'POST',
}) })
location.reload() logout()
} }
const problems = Array.from({ length: 20 }, (_, i) => ({ const problems = Array.from({ length: 20 }, (_, i) => ({
@ -24,10 +28,10 @@ export const HomePage = () => {
<main class="home"> <main class="home">
<div class="logo">PHC / Problemi</div> <div class="logo">PHC / Problemi</div>
<div class="subtitle"> <div class="subtitle">
{username ? ( {user ? (
<> <>
Logged in as {username} ( Logged in as {user.username} (
<span class="link" onClick={() => logout()}> <span class="link" onClick={handleLogout}>
Logout Logout
</span> </span>
) )

@ -1,3 +1,5 @@
import { route } from 'preact-router'
import { useState } from 'preact/hooks' import { useState } from 'preact/hooks'
export const LoginPage = () => { export const LoginPage = () => {
@ -14,7 +16,7 @@ export const LoginPage = () => {
}), }),
}) })
location.href = '/#/' route('/')
} }
return ( return (

@ -18,10 +18,11 @@
rel="stylesheet" rel="stylesheet"
/> />
<link rel="stylesheet" href="./node_modules/katex/dist/katex.css" /> <link rel="stylesheet" href="/node_modules/katex/dist/katex.css" />
<link rel="stylesheet" href="./styles/main.scss" /> <link rel="stylesheet" href="/client/styles/main.scss" />
</head> </head>
<body> <body>
<script type="module" src="./src/main.jsx"></script> <!-- SSR OUTLET -->
<script type="module" src="/client/entry-client.jsx"></script>
</body> </body>
</html> </html>

@ -5,9 +5,11 @@
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start:frontend": "cd frontend && npm run dev", "dev": "MODE=development node server.js",
"start:server": "node ./main.js", "build": "run-s build:client build:server",
"start": "concurrently npm:start:server npm:start:frontend" "build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr client/entry-server.jsx --outDir dist/server",
"serve": "node server.js"
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

@ -3,28 +3,79 @@ import fs from 'fs/promises'
import { createServer as createViteServer } from 'vite' import { createServer as createViteServer } from 'vite'
import express from 'express' import express from 'express'
import { createApiRouter } from './server/routers.js'
const CONSTANTS = { const config = {
MODE_DEVELOPMENT: 'development', isDevelopment: process.env.MODE === 'development',
port: process.env.PORT || 3000,
} }
async function createDevelopmentServer(app) { console.dir(config)
async function createDevRouter() {
const r = express.Router()
const vite = await createViteServer({ const vite = await createViteServer({
server: { middlewareMode: true }, server: { middlewareMode: true },
appType: 'custom', appType: 'custom',
}) })
app.use(vite.middlewares) r.use(vite.middlewares)
r.use('*', async (req, res, next) => {
try {
const indexHtml = await fs.readFile(path.resolve('./index.html'), 'utf-8')
const transformedTemplate = await vite.transformIndexHtml(req.originalUrl, indexHtml)
// Load (to be bundled) entry point for server side rendering
const { render } = await vite.ssrLoadModule('./client/entry-server.jsx')
app.use('*', async (req, res) => { const html = transformedTemplate.replace('<!-- SSR OUTLET -->', render(req.originalUrl))
// serve index.html
const indexHtml = await fs.readFile(path.resolve('./client/index.html'), 'utf-8') res.send(html)
const transformedIndexHtml = vite.transformIndexHtml(req.originalUrl, indexHtml) } catch (error) {
vite.ssrFixStacktrace(error)
next(error)
}
}) })
return r
} }
function createProductionServer() { async function createProductionRouter() {
app.use('/', express.static('client/dist')) // Load bundled entry point for server side rendering
const { render } = await import('./dist/server/entry-server.js')
const r = new express.Router()
r.use('/', express.static('dist/client'))
r.use('*', async (req, res) => {
const transformedTemplate = await fs.readFile(
path.resolve('./dist/client/index.html'),
'utf-8'
)
const html = transformedTemplate.replace('<!-- SSR OUTLET -->', render(req.originalUrl))
res.send(html)
})
return r
} }
async function main() {
const app = express() const app = express()
app.use('/', await createApiRouter())
if (config.isDevelopment) {
app.use('/', await createDevRouter())
} else {
app.use('/', await createProductionRouter())
}
app.listen(config.port, () => {
console.log(`Listening on port ${config.port}...`)
})
}
main()

@ -0,0 +1,7 @@
export const initialDatabaseValue = {
users: {
['BachoSeven']: {},
['aziis98']: {},
},
problems: {},
}

@ -1,79 +0,0 @@
import express from 'express'
import crypto from 'crypto'
// import serveStatic from 'serve-static'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import { authMiddleware, loggingMiddleware, PingRouter, StatusRouter } from './server/routes.js'
import { createDatabase, getUser, updateUser } from './server/db/database.js'
const app = express()
const sessions = {
store: {},
createSession(username) {
const sid = crypto.randomBytes(10).toString('hex')
this.store[sid] = username
return sid
},
getUserForSession(sid) {
return this.store[sid] ?? null
},
}
const db = createDatabase('./db.local.json', {
users: {
['BachoSeven']: {},
['aziis98']: {},
},
problems: {},
})
app.use(bodyParser.json())
app.use(cookieParser())
app.use(loggingMiddleware)
app.use(authMiddleware(sid => sessions.getUserForSession(sid)))
app.use('/api/status', new StatusRouter())
app.use('/api/ping', new PingRouter())
app.get('/api/current-user', (req, res) => {
res.json(sessions.getUserForSession(req.cookies.sid))
})
app.post('/api/login', (req, res) => {
const { username } = req.body
res.cookie('sid', sessions.createSession(username), { maxAge: 1000 * 60 * 60 * 24 * 7 })
res.json({ status: 'ok' })
})
app.post('/api/logout', (req, res) => {
res.cookie('sid', '', { expires: new Date() })
res.json({ status: 'ok' })
})
app.get('/api/user/:id', async (req, res) => {
const user = await getUser(db, req.params.id)
if (user) {
res.json(user)
} else {
res.sendStatus(404)
}
})
app.post('/api/user/:id', async (req, res) => {
await updateUser(db, req.params.id, req.body)
res.sendStatus(200)
})
app.all('*', (_req, res) => {
res.sendStatus(404)
})
app.listen(4000, () => {
console.log(`Started server on port 4000...`)
})

@ -0,0 +1,86 @@
import crypto from 'crypto'
import bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import { authMiddleware, loggingMiddleware, PingRouter, StatusRouter } from './middlewares.js'
import { createDatabase, getUser, updateUser } from './db/database.js'
import express from 'express'
import { initialDatabaseValue } from './db/example-data.js'
import { useId } from 'preact/hooks'
export async function createApiRouter() {
const sessions = {
store: {},
createSession(username) {
const sid = crypto.randomBytes(10).toString('hex')
this.store[sid] = username
return sid
},
getUserForSession(sid) {
return this.store[sid] ?? null
},
}
const db = createDatabase('./db.local.json', initialDatabaseValue)
const r = express.Router()
r.use(bodyParser.json())
r.use(cookieParser())
r.use(loggingMiddleware)
r.use(authMiddleware(sid => sessions.getUserForSession(sid)))
r.use('/api/status', new StatusRouter())
r.use('/api/ping', new PingRouter())
r.get('/api/current-user', async (req, res) => {
const userId = sessions.getUserForSession(req.cookies.sid)
if (!userId) {
res.cookie('sid', '', { expires: new Date() })
res.status(400)
res.end('Invalid session token')
return
}
const user = await getUser(db, userId)
res.json({
username: userId,
...user,
})
})
r.post('/api/login', (req, res) => {
const { username } = req.body
res.cookie('sid', sessions.createSession(username), { maxAge: 1000 * 60 * 60 * 24 * 7 })
res.json({ status: 'ok' })
})
r.post('/api/logout', (req, res) => {
res.cookie('sid', '', { expires: new Date() })
res.json({ status: 'ok' })
})
r.get('/api/user/:id', async (req, res) => {
const user = await getUser(db, req.params.id)
if (user) {
res.json(user)
} else {
res.sendStatus(404)
}
})
r.post('/api/user/:id', async (req, res) => {
await updateUser(db, req.params.id, req.body)
res.sendStatus(200)
})
// r.all('*', (_req, res) => {
// res.sendStatus(404)
// })
return r
}
Loading…
Cancel
Save