mirror of
https://github.com/tale/headplane.git
synced 2026-08-19 01:16:18 +00:00
chore: switch to react-router v7
This commit is contained in:
+15
-15
@@ -1,12 +1,12 @@
|
||||
// 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'
|
||||
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'
|
||||
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"
|
||||
@@ -14,20 +14,20 @@ env.NODE_ENV = 'development'
|
||||
// the development server can function correctly and override the production
|
||||
// values.
|
||||
|
||||
log('DEVX', 'INFO', 'Creating Vite Development Server')
|
||||
log('DEVX', 'INFO', 'Creating Vite Development Server');
|
||||
const server = await createServer({
|
||||
server: {
|
||||
middlewareMode: true
|
||||
}
|
||||
})
|
||||
middlewareMode: true,
|
||||
},
|
||||
});
|
||||
|
||||
// This entrypoint is defined in the documentation to load the server
|
||||
const build = await server.ssrLoadModule('virtual:remix/server-build')
|
||||
const build = await server.ssrLoadModule('virtual:react-router/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
|
||||
global.PREFIX = server.config.base.slice(0, -1);
|
||||
global.BUILD = build;
|
||||
global.MODE = 'development';
|
||||
global.MIDDLEWARE = server.middlewares;
|
||||
|
||||
await import('./prod.mjs')
|
||||
await import('./prod.mjs');
|
||||
|
||||
+78
-76
@@ -4,69 +4,70 @@
|
||||
// we can only need this file and a Node.js installation to run the server.
|
||||
// PREFIX is defined globally, see vite.config.ts
|
||||
|
||||
import { access, constants } from 'node:fs/promises'
|
||||
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'
|
||||
import { getWss, registerWss } from './ws.mjs'
|
||||
import { access, constants } from 'node:fs/promises';
|
||||
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';
|
||||
import { getWss, registerWss } from './ws.mjs';
|
||||
|
||||
log('SRVX', '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('SRVX', 'INFO', 'Found node_modules dependencies')
|
||||
await access('./node_modules/@react-router', constants.F_OK | constants.R_OK);
|
||||
log('SRVX', 'INFO', 'Found node_modules dependencies');
|
||||
} catch (error) {
|
||||
log('SRVX', 'ERROR', 'No node_modules found. Please run `pnpm install`')
|
||||
log('SRVX', 'ERROR', error)
|
||||
process.exit(1)
|
||||
log('SRVX', 'ERROR', 'No node_modules found. Please run `pnpm install`');
|
||||
log('SRVX', 'ERROR', error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const {
|
||||
createRequestHandler: remixRequestHandler,
|
||||
createReadableStreamFromReadable,
|
||||
writeReadableStreamToWritable
|
||||
} = await import('@remix-run/node')
|
||||
const { default: mime } = await import('mime')
|
||||
writeReadableStreamToWritable,
|
||||
} = await import('@react-router/node');
|
||||
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'
|
||||
const baseDir = resolve(join(buildPath, 'client'))
|
||||
const port = env.PORT || 3000;
|
||||
const host = env.HOST || '0.0.0.0';
|
||||
const buildPath = env.BUILD_PATH || './build';
|
||||
const baseDir = resolve(join(buildPath, 'client'));
|
||||
|
||||
if (!global.BUILD) {
|
||||
try {
|
||||
await access(join(buildPath, 'server'), constants.F_OK | constants.R_OK)
|
||||
log('SRVX', 'INFO', 'Found build directory')
|
||||
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)
|
||||
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 build = await import(resolve(join(buildPath, 'server', 'index.js')));
|
||||
global.BUILD = build;
|
||||
global.MODE = 'production';
|
||||
}
|
||||
|
||||
const handler = remixRequestHandler(global.BUILD, global.MODE)
|
||||
console.log(remixRequestHandler);
|
||||
const handler = remixRequestHandler(global.BUILD, global.MODE);
|
||||
const http = createServer(async (req, res) => {
|
||||
const url = new URL(`http://${req.headers.host}${req.url}`)
|
||||
const url = new URL(`http://${req.headers.host}${req.url}`);
|
||||
|
||||
if (global.MIDDLEWARE) {
|
||||
await new Promise(resolve => {
|
||||
global.MIDDLEWARE(req, res, resolve)
|
||||
})
|
||||
await new Promise((resolve) => {
|
||||
global.MIDDLEWARE(req, res, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
if (!url.pathname.startsWith(PREFIX)) {
|
||||
res.writeHead(404)
|
||||
res.end()
|
||||
return
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to handle an issue where say we are navigating to $PREFIX
|
||||
@@ -76,10 +77,10 @@ const http = createServer(async (req, res) => {
|
||||
// URL so that Remix can handle it correctly.
|
||||
if (url.pathname === PREFIX) {
|
||||
res.writeHead(302, {
|
||||
Location: `${PREFIX}/`
|
||||
})
|
||||
res.end()
|
||||
return
|
||||
Location: `${PREFIX}/`,
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Before we pass any requests to our Remix handler we need to check
|
||||
@@ -89,44 +90,44 @@ const http = createServer(async (req, res) => {
|
||||
// To optimize this, we send them as readable streams in the node
|
||||
// response and we also set headers for aggressive caching.
|
||||
if (url.pathname.startsWith(`${PREFIX}/assets/`)) {
|
||||
const filePath = join(baseDir, url.pathname.replace(PREFIX, ''))
|
||||
const exists = existsSync(filePath)
|
||||
const stats = statSync(filePath)
|
||||
const filePath = join(baseDir, url.pathname.replace(PREFIX, ''));
|
||||
const exists = existsSync(filePath);
|
||||
const stats = statSync(filePath);
|
||||
|
||||
if (exists && stats.isFile()) {
|
||||
// Build assets are cache-bust friendly so we can cache them heavily
|
||||
if (req.url.startsWith('/build')) {
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
}
|
||||
|
||||
// Send the file as a readable stream
|
||||
const fileStream = createReadStream(filePath)
|
||||
const type = mime.getType(filePath)
|
||||
const fileStream = createReadStream(filePath);
|
||||
const type = mime.getType(filePath);
|
||||
|
||||
res.setHeader('Content-Length', stats.size)
|
||||
res.setHeader('Content-Type', type)
|
||||
fileStream.pipe(res)
|
||||
return
|
||||
res.setHeader('Content-Length', stats.size);
|
||||
res.setHeader('Content-Type', type);
|
||||
fileStream.pipe(res);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handling the request
|
||||
const controller = new AbortController()
|
||||
res.on('close', () => controller.abort())
|
||||
const controller = new AbortController();
|
||||
res.on('close', () => controller.abort());
|
||||
|
||||
const headers = new Headers()
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (!value) continue
|
||||
if (!value) continue;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const v of value) {
|
||||
headers.append(key, v)
|
||||
headers.append(key, v);
|
||||
}
|
||||
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.append(key, value)
|
||||
headers.append(key, value);
|
||||
}
|
||||
|
||||
const remixReq = new Request(url.href, {
|
||||
@@ -135,35 +136,36 @@ const http = createServer(async (req, res) => {
|
||||
signal: controller.signal,
|
||||
|
||||
// If we have a body we set a duplex and we load the body
|
||||
...(req.method !== 'GET' && req.method !== 'HEAD' ? {
|
||||
body: createReadableStreamFromReadable(req),
|
||||
duplex: 'half'
|
||||
} : {}
|
||||
)
|
||||
})
|
||||
...(req.method !== 'GET' && req.method !== 'HEAD'
|
||||
? {
|
||||
body: createReadableStreamFromReadable(req),
|
||||
duplex: 'half',
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
||||
// Pass our request to the Remix handler and get a response
|
||||
const response = await handler(remixReq, {
|
||||
ws: getWss()
|
||||
})
|
||||
ws: getWss(),
|
||||
});
|
||||
|
||||
// Handle our response and reply
|
||||
res.statusCode = response.status
|
||||
res.statusMessage = response.statusText
|
||||
res.statusCode = response.status;
|
||||
res.statusMessage = response.statusText;
|
||||
|
||||
for (const [key, value] of response.headers.entries()) {
|
||||
res.appendHeader(key, value)
|
||||
res.appendHeader(key, value);
|
||||
}
|
||||
|
||||
if (response.body) {
|
||||
await writeReadableStreamToWritable(response.body, res)
|
||||
return
|
||||
await writeReadableStreamToWritable(response.body, res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.end()
|
||||
})
|
||||
res.end();
|
||||
});
|
||||
|
||||
registerWss(http)
|
||||
registerWss(http);
|
||||
http.listen(port, host, () => {
|
||||
log('SRVX', 'INFO', `Running on ${host}:${port}`)
|
||||
})
|
||||
log('SRVX', 'INFO', `Running on ${host}:${port}`);
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
export function log(topic, level, message) {
|
||||
const date = new Date().toISOString()
|
||||
console.log(`${date} (${level}) [${topic}] ${message}`)
|
||||
const date = new Date().toISOString();
|
||||
console.log(`${date} (${level}) [${topic}] ${message}`);
|
||||
}
|
||||
|
||||
+13
-13
@@ -1,28 +1,28 @@
|
||||
// The Websocket server is wholly responsible for ingesting messages from
|
||||
// Headplane agent instances (hopefully not more than 1 is running lol)
|
||||
import { WebSocketServer } from 'ws'
|
||||
import { log } from './utils.mjs'
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { log } from './utils.mjs';
|
||||
|
||||
const wss = new WebSocketServer({ noServer: true })
|
||||
const wss = new WebSocketServer({ noServer: true });
|
||||
wss.on('connection', (ws, req) => {
|
||||
// On connection the agent will send its NodeID via Headers
|
||||
// We store this for later use to validate and show on the UI
|
||||
const nodeID = req.headers['x-headplane-ts-node-id']
|
||||
const nodeID = req.headers['x-headplane-ts-node-id'];
|
||||
if (!nodeID) {
|
||||
ws.close(1008, 'ERR_NO_HP_TS_NODE_ID')
|
||||
return
|
||||
ws.close(1008, 'ERR_NO_HP_TS_NODE_ID');
|
||||
return;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export async function registerWss(server) {
|
||||
log('SRVX', 'INFO', 'Registering Websocket Server')
|
||||
log('SRVX', 'INFO', 'Registering Websocket Server');
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
wss.handleUpgrade(request, socket, head, ws => {
|
||||
wss.emit('connection', ws, request)
|
||||
})
|
||||
})
|
||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
wss.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getWss() {
|
||||
return wss
|
||||
return wss;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user