Initial commit

This commit is contained in:
forceoranj
2021-10-16 01:53:56 +02:00
commit ebdd8dccdd
104 changed files with 29770 additions and 0 deletions

28
src/server/devServer.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Express } from "express"
import chalk from "chalk"
import config from "../config"
export default (app: Express): void => {
const webpack = require("webpack")
const webpackConfig = require("../../webpack/client.config").default
const compiler = webpack(webpackConfig)
const instance = require("webpack-dev-middleware")(compiler, {
headers: { "Access-Control-Allow-Origin": "*" },
serverSideRender: true,
})
app.use(instance)
app.use(
require("webpack-hot-middleware")(compiler, {
log: false,
path: "/__webpack_hmr",
heartbeat: 10 * 1000,
})
)
instance.waitUntilValid(() => {
const url = `http://${config.HOST}:${config.PORT}`
console.info(chalk.green(`==> 🌎 Listening at ${url}`))
})
}

42
src/server/index.ts Executable file
View File

@@ -0,0 +1,42 @@
import path from "path"
import express from "express"
import logger from "morgan"
import compression from "compression"
import helmet from "helmet"
import hpp from "hpp"
import favicon from "serve-favicon"
import chalk from "chalk"
import devServer from "./devServer"
import ssr from "./ssr"
import { getJAVGameList } from "../gsheets/jav"
import config from "../config"
const app = express()
// Use helmet to secure Express with various HTTP headers
app.use(helmet({ contentSecurityPolicy: false }))
// Prevent HTTP parameter pollution
app.use(hpp())
// Compress all requests
app.use(compression())
// Use for http request debug (show errors only)
app.use(logger("dev", { skip: (_, res) => res.statusCode < 400 }))
app.use(favicon(path.resolve(process.cwd(), "public/favicon.ico")))
app.use(express.static(path.resolve(process.cwd(), "public")))
// Enable dev-server in development
if (__DEV__) devServer(app)
// Google Sheets requests
app.get("/javGames", getJAVGameList)
// Use React server-side rendering middleware
app.get("*", ssr)
// @ts-expect-error
app.listen(config.PORT, config.HOST, (error) => {
if (error) console.error(chalk.red(`==> 😭 OMG!!! ${error}`))
})

63
src/server/renderHtml.ts Executable file
View File

@@ -0,0 +1,63 @@
import { ChunkExtractor } from "@loadable/server"
import { HelmetData } from "react-helmet"
import serialize from "serialize-javascript"
import { minify } from "html-minifier"
export default (
head: HelmetData,
extractor: ChunkExtractor,
htmlContent: string,
initialState: typeof window.__INITIAL_STATE__
): any => {
const html = `
<!doctype html>
<html ${head.htmlAttributes.toString()}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000" />
<link rel="icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
${head.title.toString()}
${head.base.toString()}
${head.meta.toString()}
${head.link.toString()}
<!-- Insert bundled styles into <link> tag -->
${extractor.getLinkTags()}
${extractor.getStyleTags()}
</head>
<body>
<!-- Insert the router, which passed from server-side -->
<div id="react-view">${htmlContent}</div>
<!-- Store the initial state into window -->
<script>
// Use serialize-javascript for mitigating XSS attacks. See the following security issues:
// http://redux.js.org/docs/recipes/ServerRendering.html#security-considerations
window.__INITIAL_STATE__=${serialize(initialState)};
</script>
<!-- Insert bundled scripts into <script> tag -->
${extractor.getScriptTags()}
${head.script.toString()}
</body>
</html>
`
// html-minifier configuration, refer to "https://github.com/kangax/html-minifier" for more configuration
const minifyConfig = {
collapseWhitespace: true,
removeComments: true,
trimCustomFragments: true,
minifyCSS: true,
minifyJS: true,
minifyURLs: true,
}
// Minify HTML in production
return __DEV__ ? html : minify(html, minifyConfig)
}

83
src/server/ssr.tsx Normal file
View File

@@ -0,0 +1,83 @@
import path from "path"
import { renderToString } from "react-dom/server"
import { StaticRouter } from "react-router-dom"
import { renderRoutes, matchRoutes } from "react-router-config"
import { Provider } from "react-redux"
import { ChunkExtractor } from "@loadable/server"
import { Helmet } from "react-helmet"
import chalk from "chalk"
import { Request, Response, NextFunction } from "express"
import { Action } from "@reduxjs/toolkit"
import createStore from "../store"
import renderHtml from "./renderHtml"
import routes from "../routes"
export default async (req: Request, res: Response, next: NextFunction): Promise<void> => {
const { store } = createStore({ url: req.url })
// The method for loading data from server-side
const loadBranchData = (): Promise<any> => {
const branch = matchRoutes(routes, req.path)
const promises = branch.map(({ route, match }) => {
if (route.loadData)
return Promise.all(
route
.loadData({
params: match.params,
getState: store.getState,
req,
res,
})
.map((item: Action) => store.dispatch(item))
)
return Promise.resolve(null)
})
return Promise.all(promises)
}
try {
// Load data from server-side first
await loadBranchData()
const statsFile = path.resolve(process.cwd(), "public/loadable-stats")
const extractor = new ChunkExtractor({ statsFile })
const staticContext: Record<string, any> = {}
const App = extractor.collectChunks(
<Provider store={store}>
{/* Setup React-Router server-side rendering */}
<StaticRouter location={req.path} context={staticContext}>
{renderRoutes(routes)}
</StaticRouter>
</Provider>
)
const initialState = store.getState()
const htmlContent = renderToString(App)
// head must be placed after "renderToString"
// see: https://github.com/nfl/react-helmet#server-usage
const head = Helmet.renderStatic()
// Check if the render result contains a redirect, if so we need to set
// the specific status and redirect header and end the response
if (staticContext.url) {
res.status(301).setHeader("Location", staticContext.url)
res.end()
return
}
// Pass the route and initial state into html template, the "statusCode" comes from <NotFound />
res.status(staticContext.statusCode === "404" ? 404 : 200).send(
renderHtml(head, extractor, htmlContent, initialState)
)
} catch (error) {
res.status(404).send("Not Found :(")
console.error(chalk.red(`==> 😭 Rendering routes error: ${error}`))
}
next()
}