devserver/src/server.ts

153 lines
3.9 KiB
TypeScript

// Adapted from https://deno.land/std@0.106.0/http/file_server.ts
import {
listenAndServe,
posix,
readAll,
serveFile,
ServerRequest,
ServerResponse,
Sys,
} from "../deps.ts";
export function runDevServer(): void {
const target = posix.resolve("./web/");
const handler = async (req: ServerRequest) => {
let response: ServerResponse | undefined;
try {
const normalizedUrl = normalizeURL(req.url);
let fsPath = posix.join(target, normalizedUrl);
if (fsPath.indexOf(target) !== 0) {
fsPath = target;
}
const srcPath = fsPath.replace(/\/web\/(.*)\.js$/, "/$1.ts");
if (srcPath != fsPath) {
// TODO check only local
await bundle(srcPath, fsPath);
}
const fileInfo = await Deno.stat(fsPath);
if (fileInfo.isDirectory) {
response = await serveFile(req, posix.join(fsPath, "index.html"));
} else {
response = await serveFile(req, fsPath);
}
} catch (e) {
console.error(e.message);
response = await serveFallback(req, e);
} finally {
serverLog(req, response!);
try {
await req.respond(response!);
} catch (e) {
console.error(e.message);
}
}
};
const port = 4507;
const host = "0.0.0.0";
const addr = `${host}:${port}`;
listenAndServe(addr, handler);
console.log(`Dev server listening on http://${addr}/`);
}
export function normalizeURL(url: string): string {
let normalizedUrl = url;
try {
normalizedUrl = decodeURI(normalizedUrl);
} catch (e) {
if (!(e instanceof URIError)) {
throw e;
}
}
try {
//allowed per https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html
const absoluteURI = new URL(normalizedUrl);
normalizedUrl = absoluteURI.pathname;
} catch (e) { //wasn't an absoluteURI
if (!(e instanceof TypeError)) {
throw e;
}
}
if (normalizedUrl[0] !== "/") {
throw new URIError("The request URI is malformed.");
}
normalizedUrl = posix.normalize(normalizedUrl);
const startOfParams = normalizedUrl.indexOf("?");
return startOfParams > -1
? normalizedUrl.slice(0, startOfParams)
: normalizedUrl;
}
const encoder = new TextEncoder();
function serveFallback(_req: ServerRequest, e: Error): Promise<ServerResponse> {
if (e instanceof URIError) {
return Promise.resolve({
status: 400,
body: encoder.encode("Bad Request"),
});
} else if (e instanceof Deno.errors.NotFound) {
return Promise.resolve({
status: 404,
body: encoder.encode("Not Found"),
});
} else {
return Promise.resolve({
status: 500,
body: encoder.encode("Internal server error"),
});
}
}
function serverLog(req: ServerRequest, res: ServerResponse): void {
const d = new Date().toISOString();
const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`;
const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`;
console.log(s);
}
async function run(...cmd: string[]): Promise<boolean> {
const process = Sys.run({ cmd });
const result = await process.status();
return result.success;
}
async function deno(cmd: string, ...args: string[]): Promise<boolean> {
const flags = await readConfigFlags(cmd);
return await run("deno", cmd, ...flags, ...args);
}
async function readConfigFlags(cmd: string): Promise<string[]> {
const path = `config/${cmd}.flags`;
try {
const f = await Sys.open(path);
const content = await readAll(f);
return new TextDecoder()
.decode(content)
.split(" ")
.filter((part) => part && part.startsWith("--"));
} catch {
}
return [];
}
async function bundle(
source: string,
destination: string,
): Promise<void> {
if (
await deno("bundle", "-q", source, destination) &&
await deno("fmt", "-q", destination)
) {
console.log(`Bundled : ${source} -> ${destination}`);
} else {
console.error(`Bundle failed for: ${source}`);
}
}