Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create SSR production server #178

Merged
merged 8 commits into from
Feb 22, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
split servers
willybrauner committed Feb 22, 2024
commit fe85b9d57b84bc24e537c6753561c41cb38a64f0
4 changes: 2 additions & 2 deletions apps/front/package.json
Original file line number Diff line number Diff line change
@@ -4,15 +4,15 @@
"main": "src/index.tsx",
"type": "module",
"scripts": {
"dev": "node server.js",
"dev": "node server/server.dev.js",
"build:client": "vite build --ssrManifest --outDir dist/client",
"build:server": "vite build --ssr src/server/index-server.tsx --outDir dist/server",
"build:scripts": "vite build -c vite.scripts.config.ts",
"build:static": "vite build --outDir dist/static --ssrManifest",
"build": "npm run build:server && npm run build:client && npm run build:scripts && npm run build:static",
"generate": "node dist/_scripts/exe-prerender.js",
"preview": "serve dist/static",
"start": "cross-env NODE_ENV=production node server",
"start": "cross-env NODE_ENV=production node server/server.prod",
"test:watch": "vitest",
"test": "vitest run"
},
94 changes: 16 additions & 78 deletions apps/front/server.js → apps/front/server/server.dev.js
Original file line number Diff line number Diff line change
@@ -2,40 +2,37 @@ import debug from "@cher-ami/debug"
import express from "express"
import * as https from "https"
import * as mfs from "@cher-ami/mfs"
import fs from "node:fs/promises"
import portFinderSync from "portfinder-sync"
import { renderToPipeableStream } from "react-dom/server"
import { createServer, loadEnv } from "vite"
import config from "./config/config.js"

const log = debug("server:server")
import config from "../config/config.js"
const log = debug("server:server.dev")

const loadEnvVars = loadEnv(process.env.NODE_ENV, process.cwd(), "")
const IS_PROD = process.env.NODE_ENV === "production"
const BASE = loadEnvVars.VITE_APP_BASE || process.env.VITE_APP_BASE || "/"
const PROTOCOL = loadEnvVars.PROTOCOL ?? "http"
const IS_SSL = PROTOCOL === "https"
const PORT =
(loadEnvVars.DOCKER_NODE_PORT || process.env.DOCKER_NODE_PORT) ??
portFinderSync.getPort(5173)
const PORT = process.env.DOCKER_NODE_PORT ?? portFinderSync.getPort(5173)
const INDEX_SERVER_PATH = `${config.srcDir}/server/index-server.tsx`

;(async () => {
// --------------------------------------------------------------------------- SSL

/**
* Get cert and key for https
*/
let KEY, CERT
if (IS_SSL) {
if (!(await mfs.fileExists("key.pem")) || !(await mfs.fileExists("cert.pem"))) {
console.error(
"You need to generate a key and a cert file with openssl in the apps/front/ directory. Follow the README documentation 'setup-local-ssl'."
)
// prettier-ignore
console.error("You need to generate a key and a cert file with openssl in the apps/front/ directory. Follow the README documentation 'setup-local-ssl'.")
process.exit(1)
}
KEY = await mfs.readFile("key.pem")
CERT = await mfs.readFile("cert.pem")
}

// ---------------------------------------------------------------------------
// --------------------------------------------------------------------------- SERVER

/**
* Dev server
@@ -58,12 +55,13 @@ const PORT =
appType: "custom",
base: BASE
})

// use vite's connect instance as middleware
app.use(vite.middlewares)
app.use("*", async (req, res, next) => {
try {
// Transforms the ESM source code to be usable in Node.js
const { render } = await vite.ssrLoadModule(`${config.srcDir}/server/index-server.tsx`)
const { render } = await vite.ssrLoadModule(INDEX_SERVER_PATH)
// dev script to inject
const devScripts = {
js: [{ tag: "script", attr: { type: "module", src: "/src/index.tsx" } }]
@@ -88,78 +86,18 @@ const PORT =
}
})

let SSL_SERVER
let sslServer
if (IS_SSL) {
SSL_SERVER = https.createServer({ key: KEY, cert: CERT }, app)
SSL_SERVER.on("error", (error) => {
sslServer = https.createServer({ key: KEY, cert: CERT }, app)
sslServer.on("error", (error) => {
log(`Error on server: ${error}`)
})
}

return { vite, app, sslServer: SSL_SERVER }
return { vite, app, sslServer }
}

// ---------------------------------------------------------------------------

/**
* Production server
*
*/
// prettier-ignore
async function createProdServer() {
const app = express()
const compression = (await import("compression")).default
const sirv = (await import("sirv")).default
app.use(compression())
app.use(BASE, sirv("./dist/client", { extensions: [] }))
app.use("*", async (req, res) => {
try {
// Prepare scripts to inject in template
const { ManifestParser } = await import(`${config.outDirScripts}/ManifestParser.js`)
const manifest = await fs.readFile(`${config.outDirClient}/.vite/manifest.json`,"utf-8")
const scriptTags = ManifestParser.getScriptTagFromManifest(manifest, BASE)
// Prepare & stream the DOM
const { render } = await import(`${config.outDirServer}/index-server.js`)
const dom = await render(req.originalUrl, scriptTags, false, BASE)
const stream = renderToPipeableStream(dom, {
onShellReady() {
res.setHeader("Content-type", "text/html")
res.statusCode = 200
stream.pipe(res)
},
onError(e) {
res.statusCode = 500
console.error(e)
}
})
} catch (e) {
console.log(e.stack)
res.status(500).end(e.stack)
}
})

let SSL_SERVER
if (IS_SSL) {
SSL_SERVER = https.createServer({ key: KEY, cert: CERT }, app)
SSL_SERVER.on("error", (error) => {
log(`Error on server: ${error}`)
})
}
return { app, sslServer: SSL_SERVER }
}

// ---------------------------------------------------------------------------

if (!IS_PROD) {
createDevServer().then(
({ app, sslServer }) => (sslServer ?? app).listen(PORT),
() => {}
)
} else {
createProdServer().then(({ app, sslServer }) =>
(sslServer ?? app).listen(PORT, () => {
console.log(`Server started at http://localhost:${PORT}`)
})
)
}
createDevServer().then(({ app, sslServer }) => (sslServer ?? app).listen(PORT))
})()
68 changes: 68 additions & 0 deletions apps/front/server/server.prod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import express from "express"
import fs from "node:fs/promises"
import portFinderSync from "portfinder-sync"
import { renderToPipeableStream } from "react-dom/server"
import { loadEnv } from "vite"
import config from "../config/config.js"

const loadEnvVars = loadEnv(process.env.NODE_ENV, process.cwd(), "")
const BASE = loadEnvVars.VITE_APP_BASE || process.env.VITE_APP_BASE || "/"
const PORT = process.env.DOCKER_NODE_PORT ?? portFinderSync.getPort(5173)
const MANIFEST_PARSER_PATH = `${config.outDirScripts}/ManifestParser.js`
const VITE_MANIFEST_PATH = `${config.outDirClient}/.vite/manifest.json`
const INDEX_SERVER_PATH = `${config.outDirServer}/index-server.js`
const CLIENT_PATH = "./dist/client"

;(async () => {
// --------------------------------------------------------------------------- SERVER

/**
* Production server
*
*/
async function createProdServer() {
const app = express()
const compression = (await import("compression")).default
const sirv = (await import("sirv")).default
app.use(compression())
app.use(BASE, sirv(CLIENT_PATH, { extensions: [] }))
app.use("*", async (req, res) => {
try {
// Prepare scripts to inject in template
const { ManifestParser } = await import(MANIFEST_PARSER_PATH)
const manifest = await fs.readFile(VITE_MANIFEST_PATH, "utf-8")
const scriptTags = ManifestParser.getScriptTagFromManifest(manifest, BASE)

// Prepare & stream the DOM
const { render } = await import(INDEX_SERVER_PATH)
const dom = await render(req.originalUrl, scriptTags, false, BASE)

// stream the DOM
const stream = renderToPipeableStream(dom, {
onShellReady() {
res.setHeader("Content-type", "text/html")
res.statusCode = 200
stream.pipe(res)
},
onError(e) {
res.statusCode = 500
console.error(e)
}
})
} catch (e) {
console.log(e.stack)
res.status(500).end(e.stack)
}
})

return { app }
}

// --------------------------------------------------------------------------- START

createProdServer().then(({ app }) =>
app.listen(PORT, () => {
console.log(`Server started at http://localhost:${PORT}`)
})
)
})()
2 changes: 1 addition & 1 deletion apps/front/vite.scripts.config.ts
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ export default defineConfig(({ command, mode }) => {
},
rollupOptions: {
input: [
resolve("server.js"),
resolve("server/server.prod.js"),
resolve("prerender/prerender.ts"),
resolve("prerender/exe-prerender-server.ts"),
resolve("prerender/exe-prerender.ts"),