115 lines
3.9 KiB
JavaScript
115 lines
3.9 KiB
JavaScript
// Custom Next.js dev server that keeps live updates but drops the dev-only
|
|
// debug surface for anyone who is not on localhost.
|
|
//
|
|
// WHY THIS EXISTS
|
|
// `beta.dvorakmortonxd.dev` is an unauthenticated tunnel straight to the dev
|
|
// server, and a Next.js dev server is not meant to be public. It exposes a set
|
|
// of unauthenticated debug endpoints under `/__nextjs_*` (open-the-editor,
|
|
// stack-frame/source-map lookups, node debugger attach, server restart, …) that
|
|
// you really do not want on the internet. Next.js wires those endpoints in
|
|
// unconditionally, so `next.config.ts` cannot turn them off.
|
|
//
|
|
// This server sits in front of Next's request handler and returns 404 for
|
|
// `/__nextjs*` and source maps when the request did not arrive over loopback.
|
|
// Everything the browser needs for live updates still goes through:
|
|
// - normal app routes
|
|
// - `/_next/hmr` (the HMR / Fast Refresh WebSocket)
|
|
//
|
|
// So `localhost:3000` keeps the full dev experience, while the LAN/tunnel get
|
|
// a plain, live-reloading version of the site.
|
|
//
|
|
// Run with `npm run dev`. To bypass it entirely, use `npx next dev --turbopack`.
|
|
|
|
import { createServer } from "node:http";
|
|
import next from "next";
|
|
|
|
const dev = process.env.NODE_ENV !== "production";
|
|
const port = Number.parseInt(process.env.PORT || "3000", 10);
|
|
const hostname = process.env.HOSTNAME || "0.0.0.0";
|
|
|
|
// Next's dev-only internals all share this prefix:
|
|
// /__nextjs_launch-editor, /__nextjs_original-stack-frames,
|
|
// /__nextjs_source-map, /__nextjs_server_status,
|
|
// /__nextjs_attach-nodejs-inspector, /__nextjs_restart_dev, …
|
|
const DEV_DEBUG_PREFIX = "/__nextjs";
|
|
|
|
function isLoopback(socket) {
|
|
const address = socket.localAddress || "";
|
|
return (
|
|
address === "127.0.0.1" ||
|
|
address === "::1" ||
|
|
address === "::ffff:127.0.0.1" ||
|
|
address.startsWith("127.")
|
|
);
|
|
}
|
|
|
|
// A request that arrived through a proxy/tunnel (Cloudflare or otherwise) is
|
|
// never a local dev request, even if the proxy itself runs on this machine and
|
|
// therefore connects over loopback.
|
|
function hasForwardedHeaders(req) {
|
|
const headers = req.headers;
|
|
return Boolean(
|
|
headers["cf-connecting-ip"] ||
|
|
headers["cf-ray"] ||
|
|
headers["x-forwarded-for"] ||
|
|
headers["x-forwarded-proto"] ||
|
|
headers["forwarded"],
|
|
);
|
|
}
|
|
|
|
function isLocalDebugRequest(req) {
|
|
return isLoopback(req.socket) && !hasForwardedHeaders(req);
|
|
}
|
|
|
|
function isDevDebugRequest(req) {
|
|
let pathname = req.url || "/";
|
|
try {
|
|
pathname = new URL(req.url || "/", "http://localhost").pathname;
|
|
} catch {
|
|
// Fall back to the raw URL if it is not parseable.
|
|
}
|
|
return (
|
|
pathname.startsWith(DEV_DEBUG_PREFIX) || pathname.endsWith(".map")
|
|
);
|
|
}
|
|
|
|
let handle;
|
|
const server = createServer((req, res) => {
|
|
if (!isLocalDebugRequest(req) && isDevDebugRequest(req)) {
|
|
res.statusCode = 404;
|
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
res.setHeader("cache-control", "no-store");
|
|
res.end("Not found");
|
|
return;
|
|
}
|
|
// `handle` is assigned right after `getRequestHandler()` below; requests
|
|
// cannot arrive before `listen`, which happens after that assignment.
|
|
handle(req, res);
|
|
});
|
|
|
|
// Passing our HTTP server in lets Next attach its HMR upgrade handler to it.
|
|
const app = next({ dev, turbopack: true, hostname, port, httpServer: server });
|
|
handle = app.getRequestHandler();
|
|
|
|
await app.prepare();
|
|
|
|
server.listen(port, hostname, () => {
|
|
console.log(`> dev server ready on http://localhost:${port}`);
|
|
console.log(
|
|
"> dev debug endpoints are localhost-only (404 on the LAN/tunnel); HMR still works",
|
|
);
|
|
});
|
|
|
|
function shutdown() {
|
|
// Drop idle keep-alive sockets so Ctrl+C exits promptly instead of waiting
|
|
// on lingering connections.
|
|
server.closeAllConnections?.();
|
|
server.close(() => {
|
|
app.close().finally(() => process.exit(0));
|
|
});
|
|
// Hard exit if Next's cleanup hangs.
|
|
setTimeout(() => process.exit(0), 3000).unref();
|
|
}
|
|
|
|
process.on("SIGINT", shutdown);
|
|
process.on("SIGTERM", shutdown);
|