Got working preact SSR
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
auto-install-peers=true
|
# Needed by pnpm to work with "@preact/preset-vite"
|
||||||
|
shamefully-hoist=true
|
||||||
|
|||||||
+24
-7
@@ -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(() => {
|
||||||
<HomePage path="/" />
|
route(to, true)
|
||||||
<LoginPage path="/login" />
|
}, [])
|
||||||
<ProblemPage path="/problem/:id" />
|
|
||||||
</Router>
|
return (
|
||||||
)
|
<>
|
||||||
|
Redirecting to <pre>{to}</pre>...
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const App = ({ url }) => {
|
||||||
|
return (
|
||||||
|
<Router url={url}>
|
||||||
|
<HomePage path="/" />
|
||||||
|
<LoginPage path="/login" />
|
||||||
|
<ProblemPage path="/problem/:id" />
|
||||||
|
<Redirect default to="/" />
|
||||||
|
</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} />)
|
||||||
|
}
|
||||||
|
|||||||
+13
-4
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -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 logout = async () => {
|
const [user, logout] = useUser()
|
||||||
|
|
||||||
|
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 (
|
||||||
|
|||||||
+4
-3
@@ -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
-3
@@ -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)
|
||||||
|
|
||||||
app.use('*', async (req, res) => {
|
// Load (to be bundled) entry point for server side rendering
|
||||||
// serve index.html
|
const { render } = await vite.ssrLoadModule('./client/entry-server.jsx')
|
||||||
const indexHtml = await fs.readFile(path.resolve('./client/index.html'), 'utf-8')
|
|
||||||
const transformedIndexHtml = vite.transformIndexHtml(req.originalUrl, indexHtml)
|
const html = transformedTemplate.replace('<!-- SSR OUTLET -->', render(req.originalUrl))
|
||||||
|
|
||||||
|
res.send(html)
|
||||||
|
} catch (error) {
|
||||||
|
vite.ssrFixStacktrace(error)
|
||||||
|
next(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProductionRouter() {
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
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}...`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProductionServer() {
|
main()
|
||||||
app.use('/', express.static('client/dist'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = express()
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user