Restructure: move Vite project into app/, deploy build output to repo root
The webserver serves the repo checkout directly as static files, so index.html at the repo root must be the production build (pointing at hashed /assets/*.js) rather than the dev template (/src/main.tsx), which was being served with an unrecognized application/octet-stream MIME type and blocked by the browser. - Move src/, public/, scripts/, server/, package.json, vite.config.ts, tsconfig.json, .env(.example), and the dev index.html into app/. - Add app/scripts/deploy.mjs: copies app/dist/* into the repo root after every `vite build`, cleaning up stale hashed filenames. - Wire it into `npm run build` (app/package.json). - Update run.sh to operate from app/, and .gitignore for the new layout. - Commit the current build output (index.html, assets/, manifest, sw.js, workbox-*.js, favicon.svg, icons/, LICENSE) to the repo root. - Update README with the new project layout and deployment model.
This commit is contained in:
1 parent
5245c0a856
commit
c7b653ff6a
54 files changed
+224
-34
No files matched your search
@@ -0,0 +1,138 @@
|
||||
// Optional reverse proxy + static host for the built PWA.
|
||||
//
|
||||
// HERMES_TARGET=https://hermes.example.com PORT=8787 node server/proxy.mjs
|
||||
//
|
||||
// Why use it? A browser PWA served from a different origin than Hermes needs
|
||||
// the Hermes server to send CORS headers *and* allow credentialed cookies.
|
||||
// Serving the app from this proxy puts the app and the API on the same origin,
|
||||
// so the `hermes_session` cookie just works — and SSE streams pass through
|
||||
// unbuffered. It uses only Node built-ins (no dependencies).
|
||||
//
|
||||
import { createServer } from "node:http";
|
||||
import { request as httpsRequest } from "node:https";
|
||||
import { request as httpRequest } from "node:http";
|
||||
import { readFile, stat } from "node:fs/promises";
|
||||
import { extname, join, normalize, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = resolve(fileURLToPath(import.meta.url), "..");
|
||||
const DIST = resolve(__dirname, "../dist");
|
||||
const PORT = Number(process.env.PORT || 8787);
|
||||
const TARGET = process.env.HERMES_TARGET;
|
||||
// Optional: inject the API key server-side so it never reaches the browser.
|
||||
const API_KEY = process.env.HERMES_API_KEY;
|
||||
|
||||
if (!TARGET) {
|
||||
console.error("Set HERMES_TARGET to your Hermes base URL, e.g.");
|
||||
console.error(" HERMES_TARGET=https://hermes.example.com node server/proxy.mjs");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const target = new URL(TARGET);
|
||||
const isHttps = target.protocol === "https:";
|
||||
const doRequest = isHttps ? httpsRequest : httpRequest;
|
||||
|
||||
const MIME = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".webp": "image/webp",
|
||||
".ico": "image/x-icon",
|
||||
".webmanifest": "application/manifest+json",
|
||||
".woff2": "font/woff2",
|
||||
".map": "application/json",
|
||||
};
|
||||
|
||||
function proxy(req, res) {
|
||||
const headers = { ...req.headers };
|
||||
headers.host = target.host;
|
||||
if (API_KEY && !headers.authorization) {
|
||||
headers.authorization = `Bearer ${API_KEY}`;
|
||||
}
|
||||
// Let the upstream decide; we forward the original origin for CSRF checks.
|
||||
const options = {
|
||||
protocol: target.protocol,
|
||||
hostname: target.hostname,
|
||||
port: target.port || (isHttps ? 443 : 80),
|
||||
method: req.method,
|
||||
path: req.url,
|
||||
headers,
|
||||
};
|
||||
|
||||
const upstream = doRequest(options, (upRes) => {
|
||||
// Strip hop-by-hop headers.
|
||||
const outHeaders = { ...upRes.headers };
|
||||
delete outHeaders["transfer-encoding"];
|
||||
delete outHeaders["connection"];
|
||||
// Ensure SSE is not buffered by any intermediary.
|
||||
if (String(outHeaders["content-type"] || "").includes("text/event-stream")) {
|
||||
outHeaders["x-accel-buffering"] = "no";
|
||||
outHeaders["cache-control"] = "no-cache";
|
||||
}
|
||||
res.writeHead(upRes.statusCode || 502, outHeaders);
|
||||
upRes.pipe(res);
|
||||
});
|
||||
|
||||
upstream.on("error", (err) => {
|
||||
res.writeHead(502, { "content-type": "application/json" });
|
||||
res.end(JSON.stringify({ error: `Upstream error: ${err.message}` }));
|
||||
});
|
||||
|
||||
req.pipe(upstream);
|
||||
}
|
||||
|
||||
async function serveStatic(req, res) {
|
||||
let pathname = decodeURIComponent((req.url || "/").split("?")[0]);
|
||||
if (pathname === "/") pathname = "/index.html";
|
||||
const filePath = normalize(join(DIST, pathname));
|
||||
|
||||
if (!filePath.startsWith(DIST)) {
|
||||
res.writeHead(403);
|
||||
res.end("Forbidden");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await stat(filePath);
|
||||
if (info.isDirectory()) throw new Error("dir");
|
||||
const data = await readFile(filePath);
|
||||
const type = MIME[extname(filePath)] || "application/octet-stream";
|
||||
const immutable = pathname.startsWith("/assets/");
|
||||
res.writeHead(200, {
|
||||
"content-type": type,
|
||||
"cache-control": immutable ? "public, max-age=31536000, immutable" : "no-cache",
|
||||
});
|
||||
res.end(data);
|
||||
} catch {
|
||||
// SPA fallback.
|
||||
try {
|
||||
const html = await readFile(join(DIST, "index.html"));
|
||||
res.writeHead(200, { "content-type": MIME[".html"], "cache-control": "no-cache" });
|
||||
res.end(html);
|
||||
} catch {
|
||||
res.writeHead(404, { "content-type": "text/plain" });
|
||||
res.end("Not found. Did you run `npm run build` first?");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
const path = req.url || "";
|
||||
if (path.startsWith("/api/") || path.startsWith("/v1/") || path.startsWith("/health")) {
|
||||
proxy(req, res);
|
||||
} else {
|
||||
void serveStatic(req, res);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Hermes PWA proxy listening on http://localhost:${PORT}`);
|
||||
console.log(` serving: ${DIST}`);
|
||||
console.log(` proxying: /api/*, /v1/* -> ${target.origin}`);
|
||||
if (API_KEY) console.log(" injecting: Authorization: Bearer <HERMES_API_KEY>");
|
||||
});
|
||||
Reference in new issue
Block a user