From ddad7fe7467770dd68cda4ef21aed9bb7a3fd9dc Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Mon, 7 Nov 2022 02:02:23 +0100 Subject: [PATCH] Got working preact SSR --- .npmrc | 3 +- client/App.jsx | 31 +++++++--- client/Router.jsx | 79 ------------------------- client/components/Problem.jsx | 5 +- client/entry-client.jsx | 4 ++ client/entry-server.jsx | 6 ++ client/hooks.jsx | 17 ++++-- client/pages/Home.jsx | 16 ++++-- client/pages/Login.jsx | 4 +- index.html | 7 ++- package.json | 8 ++- server.js | 73 +++++++++++++++++++---- server/db/example-data.js | 7 +++ server/main.js | 79 ------------------------- server/{routes.js => middlewares.js} | 0 server/routers.js | 86 ++++++++++++++++++++++++++++ 16 files changed, 227 insertions(+), 198 deletions(-) delete mode 100644 client/Router.jsx create mode 100644 server/db/example-data.js delete mode 100644 server/main.js rename server/{routes.js => middlewares.js} (100%) create mode 100644 server/routers.js diff --git a/.npmrc b/.npmrc index 3e775ef..84c929e 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ -auto-install-peers=true +# Needed by pnpm to work with "@preact/preset-vite" +shamefully-hoist=true diff --git a/client/App.jsx b/client/App.jsx index c7dddd2..8460e66 100644 --- a/client/App.jsx +++ b/client/App.jsx @@ -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 }) => ( - - - - - -) +const Redirect = ({ to }) => { + useEffect(() => { + route(to, true) + }, []) + + return ( + <> + Redirecting to
{to}
... + + ) +} + +export const App = ({ url }) => { + return ( + + + + + + + ) +} diff --git a/client/Router.jsx b/client/Router.jsx deleted file mode 100644 index 6eb5648..0000000 --- a/client/Router.jsx +++ /dev/null @@ -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 -} - -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 {children} -} diff --git a/client/components/Problem.jsx b/client/components/Problem.jsx index 368a030..e3dbefc 100644 --- a/client/components/Problem.jsx +++ b/client/components/Problem.jsx @@ -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 }) => {
- - Problema {id} - + Problema {id}
diff --git a/client/entry-client.jsx b/client/entry-client.jsx index e69de29..bcbb351 100644 --- a/client/entry-client.jsx +++ b/client/entry-client.jsx @@ -0,0 +1,4 @@ +import { hydrate } from 'preact' +import { App } from './App.jsx' + +hydrate(, document.body) diff --git a/client/entry-server.jsx b/client/entry-server.jsx index e69de29..a8d3392 100644 --- a/client/entry-server.jsx +++ b/client/entry-server.jsx @@ -0,0 +1,6 @@ +import renderToString from 'preact-render-to-string' +import { App } from './App.jsx' + +export function render(url) { + return renderToString() +} diff --git a/client/hooks.jsx b/client/hooks.jsx index 3e941af..d5dc0f7 100644 --- a/client/hooks.jsx +++ b/client/hooks.jsx @@ -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] } diff --git a/client/pages/Home.jsx b/client/pages/Home.jsx index 93d7c19..4a70b6a 100644 --- a/client/pages/Home.jsx +++ b/client/pages/Home.jsx @@ -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 [user, logout] = useUser() - const logout = async () => { + 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 = () => {
- {username ? ( + {user ? ( <> - Logged in as {username} ( - logout()}> + Logged in as {user.username} ( + Logout ) diff --git a/client/pages/Login.jsx b/client/pages/Login.jsx index 0615eee..87132b7 100644 --- a/client/pages/Login.jsx +++ b/client/pages/Login.jsx @@ -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 ( diff --git a/index.html b/index.html index 686e6bf..460314a 100644 --- a/index.html +++ b/index.html @@ -18,10 +18,11 @@ rel="stylesheet" /> - - + + - + + diff --git a/package.json b/package.json index 92dcc1a..af6ff04 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/server.js b/server.js index 214c15f..2a8dcfa 100644 --- a/server.js +++ b/server.js @@ -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) + + // Load (to be bundled) entry point for server side rendering + const { render } = await vite.ssrLoadModule('./client/entry-server.jsx') - 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) + const html = transformedTemplate.replace('', render(req.originalUrl)) + + res.send(html) + } catch (error) { + vite.ssrFixStacktrace(error) + next(error) + } }) + + return r } -function createProductionServer() { - app.use('/', express.static('client/dist')) +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('', 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}...`) + }) } -const app = express() +main() diff --git a/server/db/example-data.js b/server/db/example-data.js new file mode 100644 index 0000000..4302361 --- /dev/null +++ b/server/db/example-data.js @@ -0,0 +1,7 @@ +export const initialDatabaseValue = { + users: { + ['BachoSeven']: {}, + ['aziis98']: {}, + }, + problems: {}, +} diff --git a/server/main.js b/server/main.js deleted file mode 100644 index 2f58025..0000000 --- a/server/main.js +++ /dev/null @@ -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...`) -}) diff --git a/server/routes.js b/server/middlewares.js similarity index 100% rename from server/routes.js rename to server/middlewares.js diff --git a/server/routers.js b/server/routers.js new file mode 100644 index 0000000..b4c6932 --- /dev/null +++ b/server/routers.js @@ -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 +}