Skip to content

Commit

Permalink
feat: enable v2_dev flag
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelDeBoey committed Aug 30, 2023
1 parent e229f45 commit d68458b
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 52 deletions.
22 changes: 4 additions & 18 deletions app/db.server.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import { PrismaClient } from "@prisma/client";
import invariant from "tiny-invariant";

let prisma: PrismaClient;
import { singleton } from "./singleton.server";

declare global {
var __db__: PrismaClient;
}

// this is needed because in development we don't want to restart
// the server with every change, but we want to make sure we don't
// create a new connection to the DB with every change either.
// in production we'll have a single connection to the DB.
if (process.env.NODE_ENV === "production") {
prisma = getClient();
} else {
if (!global.__db__) {
global.__db__ = getClient();
}
prisma = global.__db__;
}
// Hard-code a unique key, so we can look up the client when this module gets re-imported
const prisma = singleton("prisma", getPrismaClient);

function getClient() {
function getPrismaClient() {
const { DATABASE_URL } = process.env;
invariant(typeof DATABASE_URL === "string", "DATABASE_URL env var not set");

Expand Down
12 changes: 12 additions & 0 deletions app/singleton.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Borrowed & modified from https://github.com/jenseng/abuse-the-platform/blob/main/app/utils/singleton.ts
// Thanks @jenseng!

export const singleton = <Value>(
name: string,
valueFactory: () => Value
): Value => {
const g = global as any;
g.__singletons ??= {};
g.__singletons[name] ??= valueFactory();
return g.__singletons[name];
};
10 changes: 9 additions & 1 deletion mocks/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
const { rest } = require("msw");
const { setupServer } = require("msw/node");

const server = setupServer();
// put one-off handlers that don't really need an entire file to themselves here
const miscHandlers = [
rest.post(`${process.env.REMIX_DEV_HTTP_ORIGIN}/ping`, (req) =>
req.passthrough()
),
];

const server = setupServer(...miscHandlers);

server.listen({ onUnhandledRequest: "bypass" });
console.info("🔶 Mock server running");
Expand Down
7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
"build": "run-s build:*",
"build:remix": "remix build",
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build --bundle",
"dev": "run-p dev:*",
"dev:build": "cross-env NODE_ENV=development npm run build:server -- --watch",
"dev:remix": "cross-env NODE_ENV=development remix watch",
"dev:server": "cross-env NODE_ENV=development node --inspect --require ./node_modules/dotenv/config --require ./mocks ./build/server.js",
"dev": "remix dev --manual -c \"npm run dev:serve\"",
"dev:serve": "node --require ./mocks ./build/server.js",
"docker": "docker-compose up -d",
"format": "prettier --write .",
"format:repo": "npm run format && npm run lint:repo -- --fix",
Expand Down Expand Up @@ -40,6 +38,7 @@
"@remix-run/node": "*",
"@remix-run/react": "*",
"bcryptjs": "^2.4.3",
"chokidar": "^3.5.3",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2",
Expand Down
1 change: 1 addition & 0 deletions remix.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
module.exports = {
cacheDirectory: "./node_modules/.cache/remix",
future: {
v2_dev: true,
v2_errorBoundary: true,
v2_headers: true,
v2_meta: true,
Expand Down
82 changes: 53 additions & 29 deletions server.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import path from "path";
import fs from "node:fs";
import path from "node:path";
import url from "node:url";

import prom from "@isaacs/express-prometheus-middleware";
import { createRequestHandler } from "@remix-run/express";
import { installGlobals } from "@remix-run/node";
import type { ServerBuild } from "@remix-run/node";
import { broadcastDevReady, installGlobals } from "@remix-run/node";
import chokidar from "chokidar";
import compression from "compression";
import type { RequestHandler } from "express";
import express from "express";
import morgan from "morgan";
import sourceMapSupport from "source-map-support";

sourceMapSupport.install();
installGlobals();

const BUILD_PATH = path.join(process.cwd(), "build", "index.js");
const initialBuild = await reimportServer();

Check failure on line 20 in server.ts

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022', 'esnext', 'system', 'node16', or 'nodenext', and the 'target' option is set to 'es2017' or higher.

const app = express();
const metricsApp = express();
app.use(
Expand Down Expand Up @@ -80,29 +88,23 @@ app.use(express.static("public", { maxAge: "1h" }));

app.use(morgan("tiny"));

const MODE = process.env.NODE_ENV;
const BUILD_DIR = path.join(process.cwd(), "build");

app.all(
"*",
MODE === "production"
? createRequestHandler({ build: require(BUILD_DIR) })
: (...args) => {
purgeRequireCache();
const requestHandler = createRequestHandler({
build: require(BUILD_DIR),
mode: MODE,
});
return requestHandler(...args);
},
process.env.NODE_ENV === "development"
? createDevRequestHandler(initialBuild)
: createRequestHandler({
build: initialBuild,
mode: initialBuild.mode,

Check failure on line 97 in server.ts

View workflow job for this annotation

GitHub Actions / ʦ TypeScript

Property 'mode' does not exist on type 'ServerBuild'.
}),
);

const port = process.env.PORT || 3000;

app.listen(port, () => {
// require the built app so we're ready when the first request comes in
require(BUILD_DIR);
console.log(`✅ app ready: http://localhost:${port}`);

if (process.env.NODE_ENV === "development") {
broadcastDevReady(initialBuild);
}
});

const metricsPort = process.env.METRICS_PORT || 3001;
Expand All @@ -111,16 +113,38 @@ metricsApp.listen(metricsPort, () => {
console.log(`✅ metrics ready: http://localhost:${metricsPort}/metrics`);
});

function purgeRequireCache() {
// purge require cache on requests for "server side HMR" this won't let
// you have in-memory objects between requests in development,
// alternatively you can set up nodemon/pm2-dev to restart the server on
// file changes, we prefer the DX of this though, so we've included it
// for you by default
for (const key in require.cache) {
if (key.startsWith(BUILD_DIR)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete require.cache[key];
}
async function reimportServer(): Promise<ServerBuild> {
const stat = fs.statSync(BUILD_PATH);

// convert build path to URL for Windows compatibility with dynamic `import`
const BUILD_URL = url.pathToFileURL(BUILD_PATH).href;

// use a timestamp query parameter to bust the import cache
return import(BUILD_URL + "?t=" + stat.mtimeMs);
}

function createDevRequestHandler(initialBuild: ServerBuild): RequestHandler {
let build = initialBuild;
async function handleServerUpdate() {
// 1. re-import the server build
build = await reimportServer();
// 2. tell Remix that this app server is now up-to-date and ready
broadcastDevReady(build);
}
chokidar
.watch(BUILD_PATH, { ignoreInitial: true })
.on("add", handleServerUpdate)
.on("change", handleServerUpdate);

// wrap request handler to make sure its recreated with the latest build for every request
return async (req, res, next) => {
try {
return createRequestHandler({
build,
mode: "development",
})(req, res, next);
} catch (error) {
next(error);
}
};
}

0 comments on commit d68458b

Please sign in to comment.