remix-demo
react router (remix) demo
git clone https://9o.is/git/remix-demo.git
server.ts
(3256B)
1 import path from "path";
2 import express from "express";
3 import compression from "compression";
4 import morgan from "morgan";
5 import { createRequestHandler } from "@remix-run/express";
6
7 const app = express();
8
9 app.use((req, res, next) => {
10 // helpful headers:
11 res.set("x-fly-region", process.env.FLY_REGION ?? "unknown");
12 res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`);
13
14 // /clean-urls/ -> /clean-urls
15 if (req.path.endsWith("/") && req.path.length > 1) {
16 const query = req.url.slice(req.path.length);
17 const safepath = req.path.slice(0, -1).replace(/\/+/g, "/");
18 res.redirect(301, safepath + query);
19 return;
20 }
21 next();
22 });
23
24 // if we're not in the primary region, then we need to make sure all
25 // non-GET/HEAD/OPTIONS requests hit the primary region rather than read-only
26 // Postgres DBs.
27 // learn more: https://fly.io/docs/getting-started/multi-region-databases/#replay-the-request
28 app.all("*", function getReplayResponse(req, res, next) {
29 const { method, path: pathname } = req;
30 const { PRIMARY_REGION, FLY_REGION } = process.env;
31
32 const isMethodReplayable = !["GET", "OPTIONS", "HEAD"].includes(method);
33 const isReadOnlyRegion =
34 FLY_REGION && PRIMARY_REGION && FLY_REGION !== PRIMARY_REGION;
35
36 const shouldReplay = isMethodReplayable && isReadOnlyRegion;
37
38 if (!shouldReplay) return next();
39
40 const logInfo = {
41 pathname,
42 method,
43 PRIMARY_REGION,
44 FLY_REGION,
45 };
46 console.info(`Replaying:`, logInfo);
47 res.set("fly-replay", `region=${PRIMARY_REGION}`);
48 return res.sendStatus(409);
49 });
50
51 app.use(compression());
52
53 // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
54 app.disable("x-powered-by");
55
56 // Remix fingerprints its assets so we can cache forever.
57 app.use(
58 "/build",
59 express.static("public/build", { immutable: true, maxAge: "1y" })
60 );
61
62 // Everything else (like favicon.ico) is cached for an hour. You may want to be
63 // more aggressive with this caching.
64 app.use(express.static("public", { maxAge: "1h" }));
65
66 app.use(morgan("tiny"));
67
68 const MODE = process.env.NODE_ENV;
69 const BUILD_DIR = path.join(process.cwd(), "build");
70
71 app.all(
72 "*",
73 MODE === "production"
74 ? createRequestHandler({ build: require(BUILD_DIR) })
75 : (...args) => {
76 purgeRequireCache();
77 const requestHandler = createRequestHandler({
78 build: require(BUILD_DIR),
79 mode: MODE,
80 });
81 return requestHandler(...args);
82 }
83 );
84
85 const port = process.env.PORT || 3000;
86
87 app.listen(port, () => {
88 // require the built app so we're ready when the first request comes in
89 require(BUILD_DIR);
90 console.log(`✅ app ready: http://localhost:${port}`);
91 });
92
93 function purgeRequireCache() {
94 // purge require cache on requests for "server side HMR" this won't let
95 // you have in-memory objects between requests in development,
96 // alternatively you can set up nodemon/pm2-dev to restart the server on
97 // file changes, we prefer the DX of this though, so we've included it
98 // for you by default
99 for (const key in require.cache) {
100 if (key.startsWith(BUILD_DIR)) {
101 // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
102 delete require.cache[key];
103 }
104 }
105 }