Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

feat: migrate from remix compiler to vite plugin #249

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ yarn.lock
node_modules

/build
/server-build
/public/build
.env
.cache

/cypress/screenshots
/cypress/videos
Expand Down
6 changes: 1 addition & 5 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { cssBundleHref } from "@remix-run/css-bundle";
import type { LinksFunction, LoaderFunctionArgs } from "@remix-run/node";
import { json } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";

import { getUser } from "~/session.server";
import stylesheet from "~/tailwind.css";
import stylesheet from "~/tailwind.css?url";

export const links: LinksFunction = () => [
{ rel: "stylesheet", href: stylesheet },
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
];

export const loader = async ({ request }: LoaderFunctionArgs) => {
Expand All @@ -35,7 +32,6 @@ export default function App() {
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
Expand Down
6 changes: 6 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// eslint-disable-next-line no-undef
if (process.env.NODE_ENV === "production") {
await import("./server-build/index.js");
} else {
await import("./server.ts");
}
27 changes: 14 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@
"name": "blues-stack-template",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"build": "npm-run-all --sequential build:*",
"build:remix": "remix build",
"build:server": "esbuild --platform=node --format=cjs ./server.ts --outdir=build --bundle --external:fsevents",
"dev": "npm-run-all --parallel dev:*",
"dev:server": "cross-env NODE_ENV=development npm run build:server -- --watch",
"dev:remix": "remix dev --manual -c \"node --require ./mocks --watch-path ./build/server.js --watch ./build/server.js\"",
"build:remix": "remix vite:build",
"build:server": "tsx ./server/build-server.ts",
"dev": "NODE_ENV=development node ./server/dev-server.js",
"docker": "docker compose up -d",
"format": "prettier --write .",
"format:repo": "npm run format && npm run lint -- --fix",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint .",
"setup": "prisma generate && prisma migrate deploy && prisma db seed",
"start": "cross-env NODE_ENV=production node ./build/server.js",
"start:mocks": "cross-env NODE_ENV=production node --require ./mocks --require dotenv/config ./build/server.js",
"start": "cross-env NODE_ENV=production node --require dotenv/config .",
"start:mocks": "cross-env NODE_ENV=production node --require ./mocks --require dotenv/config .",
"test": "vitest",
"test:e2e:dev": "start-server-and-test dev http://localhost:3000 \"npx cypress open\"",
"pretest:e2e:run": "npm run build",
Expand All @@ -32,10 +31,9 @@
"dependencies": {
"@isaacs/express-prometheus-middleware": "^1.2.1",
"@prisma/client": "^5.20.0",
"@remix-run/css-bundle": "*",
"@remix-run/express": "*",
"@remix-run/node": "*",
"@remix-run/react": "*",
"@remix-run/express": "^2.15.2",
"@remix-run/node": "^2.15.2",
"@remix-run/react": "^2.15.2",
"bcryptjs": "^2.4.3",
"chokidar": "^3.6.0",
"compression": "^1.7.4",
Expand All @@ -59,6 +57,7 @@
"@types/cookie": "^0.6.0",
"@types/eslint": "^8.56.12",
"@types/express": "^4.17.21",
"@types/fs-extra": "^11.0.4",
"@types/morgan": "^1.9.9",
"@types/node": "^20.16.10",
"@types/react": "^18.3.11",
Expand All @@ -85,6 +84,8 @@
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-testing-library": "^6.3.0",
"execa": "^9.5.2",
"fs-extra": "^11.2.0",
"happy-dom": "^15.7.4",
"msw": "^2.4.9",
"npm-run-all2": "^6.2.3",
Expand All @@ -96,12 +97,12 @@
"tailwindcss": "^3.4.13",
"tsx": "^4.19.1",
"typescript": "^5.6.2",
"vite": "^5.4.8",
"vite": "^5.4.11",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^2.1.2"
},
"engines": {
"node": ">=18.0.0"
"node": "18.0.0"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
Expand Down
2 changes: 1 addition & 1 deletion postcss.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
Expand Down
2 changes: 1 addition & 1 deletion prettier.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @type {import("prettier").Config} */
module.exports = {
export default {
plugins: ["prettier-plugin-tailwindcss"],
};
6 changes: 0 additions & 6 deletions remix.config.js

This file was deleted.

2 changes: 0 additions & 2 deletions remix.env.d.ts

This file was deleted.

128 changes: 54 additions & 74 deletions server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
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 type { ServerBuild } from "@remix-run/node";
import { broadcastDevReady, installGlobals } from "@remix-run/node";
import { installGlobals } from "@remix-run/node";
import compression from "compression";
import type { RequestHandler } from "express";
import express from "express";
import morgan from "morgan";
import sourceMapSupport from "source-map-support";
Expand All @@ -17,17 +12,16 @@ installGlobals();
run();

async function run() {
const BUILD_PATH = path.resolve("build/index.js");
const VERSION_PATH = path.resolve("build/version.txt");

const initialBuild = await reimportServer();
const remixHandler =
process.env.NODE_ENV === "development"
? await createDevRequestHandler(initialBuild)
: createRequestHandler({
build: initialBuild,
mode: initialBuild.mode,
});
const MODE = process.env.NODE_ENV;

const viteDevServer =
MODE === "development"
? await import("vite").then((vite) =>
vite.createServer({
server: { middlewareMode: true },
}),
)
: undefined;

const app = express();
const metricsApp = express();
Expand Down Expand Up @@ -86,27 +80,43 @@ async function run() {
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
app.disable("x-powered-by");

// Remix fingerprints its assets so we can cache forever.
app.use(
"/build",
express.static("public/build", { immutable: true, maxAge: "1y" }),
);

// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static("public", { maxAge: "1h" }));
if (viteDevServer) {
app.use(viteDevServer.middlewares);
} else {
// Remix fingerprints its assets so we can cache forever.
app.use(
"/assets",
express.static("build/client/assets", { immutable: true, maxAge: "1y" }),
);
// Everything else (like favicon.ico) is cached for an hour. You may want to be
// more aggressive with this caching.
app.use(express.static("build/client", { maxAge: "1h" }));
}

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

app.all("*", remixHandler);
app.all(
"*",
createRequestHandler({
getLoadContext: (_, res) => ({
cspNonce: res.locals.cspNonce,
serverBuild: getBuild(),
}),
mode: MODE,
build: async () => {
const { error, build } = await getBuild();
// gracefully "catch" the error
if (error) {
throw error;
}
return build;
},
}),
);

const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`✅ app ready: http://localhost:${port}`);

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

const metricsPort = process.env.METRICS_PORT || 3010;
Expand All @@ -115,49 +125,19 @@ async function run() {
console.log(`✅ metrics ready: http://localhost:${metricsPort}/metrics`);
});

async function reimportServer(): Promise<ServerBuild> {
// cjs: manually remove the server build from the require cache
Object.keys(require.cache).forEach((key) => {
if (key.startsWith(BUILD_PATH)) {
delete require.cache[key];
}
});

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);
}

async function createDevRequestHandler(
initialBuild: ServerBuild,
): Promise<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);
async function getBuild() {
try {
const build = viteDevServer
? await viteDevServer.ssrLoadModule("virtual:remix/server-build")
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - the file might not exist yet but it will
await import("./build/server/index.js");

return { build: build as unknown as ServerBuild, error: null };
} catch (error) {
// Catch error and return null to make express happy and avoid an unrecoverable crash
console.error("Error creating build:", error);
return { error: error, build: null as unknown as ServerBuild };
}
const chokidar = await import("chokidar");
chokidar
.watch(VERSION_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);
}
};
}
}
29 changes: 29 additions & 0 deletions server/build-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import path from "node:path";
import { fileURLToPath } from "node:url";

import esbuild from "esbuild";
import fsExtra from "fs-extra";

const pkg = fsExtra.readJsonSync(path.join(process.cwd(), "package.json"));

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const here = (...s: string[]) => path.join(__dirname, ...s);

console.log();
console.log("building...");

esbuild
.build({
// note that we are not including dev-server.js since it's only used for development
entryPoints: [here("../server.ts")],
outdir: here("../server-build"),
target: [`node${pkg.engines.node}`],
platform: "node",
sourcemap: true,
format: "esm",
logLevel: "info",
})
.catch((error: unknown) => {
console.error(error);
process.exit(1);
});
21 changes: 21 additions & 0 deletions server/dev-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { execa } from "execa";

// eslint-disable-next-line no-undef
if (process.env.NODE_ENV === "production") {
await import("../server-build/index.js");
} else {
const command =
'tsx watch --clear-screen=false --ignore ".cache/**" --ignore "app/**" --ignore "vite.config.ts.timestamp-*" --ignore "build/**" --ignore "node_modules/**" --inspect ./index.js';
execa(command, {
stdio: ["ignore", "inherit", "inherit"],
shell: true,
env: {
FORCE_COLOR: true,
MOCKS: true,
// eslint-disable-next-line no-undef
...process.env,
},
// https://github.com/sindresorhus/execa/issues/433
windowsHide: false,
});
}
6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2020"],
"types": ["vitest/globals"],
"types": ["@remix-run/node", "vite/client"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"module": "CommonJS",
"moduleResolution": "node",
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"target": "ES2020",
"strict": true,
Expand Down
Loading