From 1c2c374adaa54dbc29b4320638397a54fa6f53b2 Mon Sep 17 00:00:00 2001 From: Aarnav Tale Date: Mon, 23 Dec 2024 10:27:05 -0500 Subject: [PATCH] feat: switch to server in dev & prod --- package.json | 4 ++- pnpm-lock.yaml | 3 ++ server/dev.mjs | 33 +++++++++++++++++++++ server.mjs => server/prod.mjs | 56 +++++++++++++++++++---------------- server/utils.mjs | 4 +++ vite.config.ts | 9 ++++-- 6 files changed, 80 insertions(+), 29 deletions(-) create mode 100644 server/dev.mjs rename server.mjs => server/prod.mjs (77%) mode change 100755 => 100644 create mode 100644 server/utils.mjs diff --git a/package.json b/package.json index 239c199..2a29c0f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "build": "remix vite:build && vite build", - "dev": "remix vite:dev", + "dev": "node server/dev.mjs", + "start": "node build/headplane/server.js", "typecheck": "tsc" }, "dependencies": { @@ -43,6 +44,7 @@ "zod": "^3.23.8" }, "devDependencies": { + "@babel/preset-typescript": "^7.26.0", "@remix-run/dev": "^2.15.0", "@remix-run/route-config": "^2.15.0", "autoprefixer": "^10.4.20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94b23ca..3bf25e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: specifier: ^3.23.8 version: 3.23.8 devDependencies: + '@babel/preset-typescript': + specifier: ^7.26.0 + version: 7.26.0(@babel/core@7.26.0) '@remix-run/dev': specifier: ^2.15.0 version: 2.15.0(@remix-run/react@2.15.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.7.2))(@types/node@22.10.1)(typescript@5.7.2)(vite@6.0.3(@types/node@22.10.1)(jiti@1.21.6)(yaml@2.6.1)) diff --git a/server/dev.mjs b/server/dev.mjs new file mode 100644 index 0000000..f204e31 --- /dev/null +++ b/server/dev.mjs @@ -0,0 +1,33 @@ +// This is a polyglot entrypoint for Headplane when running in development +// It does some silly magic to load the vite config, set some globals that +// are required to function, and create the vite development server. +import { createServer } from 'vite' +import { env, exit } from 'node:process' +import { log } from './utils.mjs' + +log('DEVX', 'INFO', 'This script is only intended for development') +env.NODE_ENV = 'development' + +// The production entrypoint uses a global called "PREFIX" to determine +// what route the application is being served at and a global called "BUILD" +// to determine the Remix handler. We need to set these globals here so that +// the development server can function correctly and override the production +// values. + +log('DEVX', 'INFO', 'Creating Vite Development Server') +const server = await createServer({ + server: { + middlewareMode: true + } +}) + +// This entrypoint is defined in the documentation to load the server +const build = await server.ssrLoadModule('virtual:remix/server-build') + +// We already handle this logic in the Vite configuration +global.PREFIX = server.config.base.slice(0, -1) +global.BUILD = build +global.MODE = 'development' +global.MIDDLEWARE = server.middlewares + +await import('./prod.mjs') diff --git a/server.mjs b/server/prod.mjs old mode 100755 new mode 100644 similarity index 77% rename from server.mjs rename to server/prod.mjs index 79d28b8..f5fd9f6 --- a/server.mjs +++ b/server/prod.mjs @@ -9,30 +9,16 @@ import { createReadStream, existsSync, statSync } from 'node:fs' import { createServer } from 'node:http' import { join, resolve } from 'node:path' import { env } from 'node:process' +import { log } from './utils.mjs' -function log(level, message) { - const date = new Date().toISOString() - console.log(`${date} (${level}) [SRVX] ${message}`) -} - -log('INFO', `Running with Node.js ${process.versions.node}`) +log('SRVX', 'INFO', `Running with Node.js ${process.versions.node}`) try { await access('./node_modules/@remix-run', constants.F_OK | constants.R_OK) - log('INFO', 'Found node_modules dependencies') + log('SRVX', 'INFO', 'Found node_modules dependencies') } catch (error) { - log('ERROR', 'No node_modules found. Please run `pnpm install` first') - log('ERROR', error) - process.exit(1) -} - -try { - await access('./build/server', constants.F_OK | constants.R_OK) - log('INFO', 'Found build directory') -} catch (error) { - const date = new Date().toISOString() - log('ERROR', 'No build directory found. Please run `pnpm build` first') - log('ERROR', error) + log('SRVX', 'ERROR', 'No node_modules found. Please run `pnpm install`') + log('SRVX', 'ERROR', error) process.exit(1) } @@ -46,16 +32,36 @@ const { default: mime } = await import('mime') const port = env.PORT || 3000 const host = env.HOST || '0.0.0.0' const buildPath = env.BUILD_PATH || './build' - -// Because this is a dynamic import without an easily discernable path -// we gain the "deoptimization" we want so that Vite doesn't bundle this -const build = await import(resolve(join(buildPath, 'server', 'index.js'))) const baseDir = resolve(join(buildPath, 'client')) -const handler = remixRequestHandler(build, 'production') +if (!global.BUILD) { + try { + await access(join(buildPath, 'server'), constants.F_OK | constants.R_OK) + log('SRVX', 'INFO', 'Found build directory') + } catch (error) { + const date = new Date().toISOString() + log('SRVX', 'ERROR', 'No build found. Please run `pnpm build`') + log('SRVX', 'ERROR', error) + process.exit(1) + } + + // Because this is a dynamic import without an easily discernable path + // we gain the "deoptimization" we want so that Vite doesn't bundle this + const build = await import(resolve(join(buildPath, 'server', 'index.js'))) + global.BUILD = build + global.MODE = 'production' +} + +const handler = remixRequestHandler(global.BUILD, global.MODE) const http = createServer(async (req, res) => { const url = new URL(`http://${req.headers.host}${req.url}`) + if (global.MIDDLEWARE) { + await new Promise(resolve => { + global.MIDDLEWARE(req, res, resolve) + }) + } + if (!url.pathname.startsWith(PREFIX)) { res.writeHead(404) res.end() @@ -155,5 +161,5 @@ const http = createServer(async (req, res) => { }) http.listen(port, host, () => { - log('INFO', `Running on ${host}:${port}`) + log('SRVX', 'INFO', `Running on ${host}:${port}`) }) diff --git a/server/utils.mjs b/server/utils.mjs new file mode 100644 index 0000000..c18fdc2 --- /dev/null +++ b/server/utils.mjs @@ -0,0 +1,4 @@ +export function log(topic, level, message) { + const date = new Date().toISOString() + console.log(`${date} (${level}) [${topic}] ${message}`) +} diff --git a/vite.config.ts b/vite.config.ts index 5b97cd9..3f27b1f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,15 +17,18 @@ if (!version) { export default defineConfig(({ isSsrBuild }) => { // If we have the Headplane entry we build it as a single - // server.mjs file that is built for production server bundle + // server/prod.mjs file that is built for production server bundle // We know the remix invoked command is vite:build - if (!process.argv.includes('vite:build') && !process.argv.includes('vite:dev')) { + if ( + process.env.NODE_ENV !== 'development' + && !process.argv.includes('vite:build') + ) { return { build: { minify: false, target: 'esnext', rollupOptions: { - input: './server.mjs', + input: './server/prod.mjs', output: { entryFileNames: 'server.js', dir: 'build/headplane',