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 { route } from 'preact-router'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
|
||||
import { HomePage } from './pages/Home.jsx'
|
||||
import { LoginPage } from './pages/Login.jsx'
|
||||
import { ProblemPage } from './pages/Problem.jsx'
|
||||
|
||||
export const App = ({ path }) => (
|
||||
<Router>
|
||||
<HomePage path="/" />
|
||||
<LoginPage path="/login" />
|
||||
<ProblemPage path="/problem/:id" />
|
||||
</Router>
|
||||
)
|
||||
const Redirect = ({ to }) => {
|
||||
useEffect(() => {
|
||||
route(to, true)
|
||||
}, [])
|
||||
|
||||
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'
|
||||
|
||||
export const Problem = ({ id, content }) => {
|
||||
@@ -6,9 +5,7 @@ export const Problem = ({ id, content }) => {
|
||||
<div class="problem">
|
||||
<div class="problem-header">
|
||||
<div class="problem-title">
|
||||
<Link page={`/problem/:id`} params={{ id }}>
|
||||
Problema {id}
|
||||
</Link>
|
||||
<a href={`/problem/${id}`}>Problema {id}</a>
|
||||
</div>
|
||||
</div>
|
||||
<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'
|
||||
|
||||
export const useUser = () => {
|
||||
const [username, setUsername] = useState(null)
|
||||
const [user, setUser] = useState(null)
|
||||
|
||||
const logout = () => {
|
||||
setUser(null)
|
||||
}
|
||||
|
||||
useEffect(async () => {
|
||||
const res = await fetch(`/api/current-user`, {
|
||||
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 { useUser } from '../hooks.jsx'
|
||||
|
||||
export const HomePage = () => {
|
||||
const { username } = useUser()
|
||||
console.log('rendering homepage')
|
||||
|
||||
const logout = async () => {
|
||||
const [user, logout] = useUser()
|
||||
|
||||
const handleLogout = async () => {
|
||||
await fetch(`/api/logout`, {
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
location.reload()
|
||||
logout()
|
||||
}
|
||||
|
||||
const problems = Array.from({ length: 20 }, (_, i) => ({
|
||||
@@ -24,10 +28,10 @@ export const HomePage = () => {
|
||||
<main class="home">
|
||||
<div class="logo">PHC / Problemi</div>
|
||||
<div class="subtitle">
|
||||
{username ? (
|
||||
{user ? (
|
||||
<>
|
||||
Logged in as {username} (
|
||||
<span class="link" onClick={() => logout()}>
|
||||
Logged in as {user.username} (
|
||||
<span class="link" onClick={handleLogout}>
|
||||
Logout
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { route } from 'preact-router'
|
||||
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export const LoginPage = () => {
|
||||
@@ -14,7 +16,7 @@ export const LoginPage = () => {
|
||||
}),
|
||||
})
|
||||
|
||||
location.href = '/#/'
|
||||
route('/')
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+4
-3
@@ -18,10 +18,11 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="./node_modules/katex/dist/katex.css" />
|
||||
<link rel="stylesheet" href="./styles/main.scss" />
|
||||
<link rel="stylesheet" href="/node_modules/katex/dist/katex.css" />
|
||||
<link rel="stylesheet" href="/client/styles/main.scss" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./src/main.jsx"></script>
|
||||
<!-- SSR OUTLET -->
|
||||
<script type="module" src="/client/entry-client.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+5
-3
@@ -5,9 +5,11 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start:frontend": "cd frontend && npm run dev",
|
||||
"start:server": "node ./main.js",
|
||||
"start": "concurrently npm:start:server npm:start:frontend"
|
||||
"dev": "MODE=development node server.js",
|
||||
"build": "run-s build:client build:server",
|
||||
"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",
|
||||
"dependencies": {
|
||||
|
||||
@@ -3,28 +3,79 @@ import fs from 'fs/promises'
|
||||
|
||||
import { createServer as createViteServer } from 'vite'
|
||||
import express from 'express'
|
||||
import { createApiRouter } from './server/routers.js'
|
||||
|
||||
const CONSTANTS = {
|
||||
MODE_DEVELOPMENT: 'development',
|
||||
const config = {
|
||||
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({
|
||||
server: { middlewareMode: true },
|
||||
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) => {
|
||||
// serve index.html
|
||||
const indexHtml = await fs.readFile(path.resolve('./client/index.html'), 'utf-8')
|
||||
const transformedIndexHtml = vite.transformIndexHtml(req.originalUrl, indexHtml)
|
||||
// Load (to be bundled) entry point for server side rendering
|
||||
const { render } = await vite.ssrLoadModule('./client/entry-server.jsx')
|
||||
|
||||
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() {
|
||||
app.use('/', express.static('client/dist'))
|
||||
}
|
||||
|
||||
const app = express()
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user