import { serveFile } from "jsr:@std/http/file-server";
import { STATUS_TEXT } from "jsr:@std/http/status";
import { type App, toValue } from "vue";
import { type Router } from "vue-router";
import { renderToString } from "vue/server-renderer";
import transpileResponse from "./transpile.ts";
import { DOMParser } from "jsr:@b-fuze/deno-dom";
import { join } from "jsr:@std/path/join";
import { exists } from "jsr:@std/fs";
import { type DJSSRContext } from "@/useDJSSRContext.ts";
import { type DJAPIResult, type DJAPIResultMap } from "@/api.ts";
const utf8Decoder = new TextDecoder("utf-8");
const parser = new DOMParser();
function appHeaderScript(params: { ssrContext: DJSSRContext, entryPath: string }) {
return `
${ toValue(params.ssrContext.head.title) }
${ toValue(params.ssrContext.head.metatags).map(_ => ``).join('\n\t') }
`;
}
async function* siteEntries(path: string): AsyncGenerator {
for await (const dirEnt of Deno.readDir(path)) {
if (dirEnt.isDirectory) {
yield* siteEntries(join(path, dirEnt.name));
} else if (dirEnt.name === "index_template.html") {
yield path.split("/")[1] ?? "";
}
}
}
const publicFiles = siteEntries("public");
const sites: string[] = [];
for await (const path of publicFiles) {
sites.push(path);
}
async function getAPIResponse(apiReq: Request): Promise {
let jsonResponse: DJAPIResult | { error: string } | null = null;
let status = 200;
const pathname = URL.parse(apiReq.url)?.pathname;
if (!pathname) {
jsonResponse = { error: "Invalid Route" };
status = 400;
}
if (!jsonResponse && pathname) {
const apiPath = pathname.split("/api")[1];
if (apiPath === "/rp-articles") {
const paths: string[] = [];
const contentDir = './public/generative-energy/content/';
for await (const dirEnt of Deno.readDir(contentDir)) {
if (dirEnt.isFile && dirEnt.name.endsWith('.html')) {
paths.push(`${contentDir}${dirEnt.name}`);
}
}
const result: DJAPIResultMap['/rp-articles'] = [];
for (const filePath of paths) {
const content = await Deno.readTextFile(filePath);
const dom = parser.parseFromString(content, 'text/html');
const metadata = { title: '', author: 'Ray Peat, übersetzt von Daniel Ledda', titleEn: '', titleDe: '', tags: [] as string[], slug: '' };
const metaTags = dom.querySelectorAll('meta') as unknown as NodeListOf;
for (const metaTag of metaTags) {
const name = metaTag.attributes.getNamedItem('name')?.value ?? '';
const content = metaTag.attributes.getNamedItem('content')?.value ?? '';
if (name === 'title-de') {
metadata.titleDe = content;
metadata.title = content;
} else if (name === 'title-en') {
metadata.titleEn = content;
} else if (name === 'tags') {
metadata.tags = content ? content.split(",") : [];
} else if (name === 'slug') {
metadata.slug = content;
}
}
result.push(metadata);
}
jsonResponse = result;
}
if (!jsonResponse) {
jsonResponse = { error: `API route ${ apiPath } not found.` };
status = 404;
}
}
const headers = new Headers();
headers.set("Content-Type", "application/json");
return new Response(JSON.stringify(jsonResponse), {
status,
headers,
});
}
const redirects = {
'/generative-energy/rp-deutsch': {
target: '/generative-energy/raypeat-deutsch',
code: 301,
},
} as const;
const redirectPaths = Object.keys(redirects) as (keyof typeof redirects)[];
Deno.serve({
port: 8080,
hostname: "0.0.0.0",
onListen({ port, hostname }) {
console.log(`Listening on port http://${hostname}:${port}/`);
},
}, async (req, _conn) => {
const timeStart = new Date().getTime();
let response: Response | null = null;
const url = URL.parse(req.url);
if (req.method === "GET") {
const pathname = url?.pathname ?? "/";
// Redirects
const redirect = redirectPaths.find(_ => pathname.startsWith(_))
if (response === null && redirect) {
const entry = redirects[redirect];
const headers = new Headers();
headers.set('Location', entry.target);
response = new Response(STATUS_TEXT[entry.code], { headers, status: entry.code });
}
// API
if (response === null && pathname.startsWith("/api/")) {
response = await getAPIResponse(req);
}
// Public/static files
if (response === null) {
let filepath = join(".", "public", pathname);
if (filepath.endsWith("/")) {
filepath = join(filepath, "index.html");
}
if (await exists(filepath, { isFile: true })) {
response = await serveFile(req, filepath);
}
}
// NPM Vendor deps
if (response === null && pathname.startsWith("/deps")) {
response = await serveFile(req, `node_modules/${pathname.split("/deps")[1]}`);
}
// Transpile Code
if (
response === null &&
pathname.startsWith("/app") &&
(pathname.endsWith(".ts") || pathname.endsWith(".tsx")) &&
!pathname.endsWith("server.ts")
) {
response = await serveFile(req, "./" + pathname);
response = await transpileResponse(response, req.url, pathname);
}
// SSR
if (response === null) {
const baseDirectoryName = pathname.split("/")[1] ?? "";
if (sites.includes(baseDirectoryName)) {
const appLocation = baseDirectoryName === "" ? "home" : baseDirectoryName;
const siteTemplate = join("public", baseDirectoryName, "index_template.html");
const siteEntry = join("app", appLocation, "server.ts");
const clientEntry = join("@", appLocation, "client.ts");
const { app, router } = (await import("./" + siteEntry)).default() as {
app: App;
router: Router | null;
};
app.provide("dom-parse", (innerHTML: string) => {
return parser.parseFromString(innerHTML, "text/html").documentElement;
});
const ssrContext: DJSSRContext = { styles: {}, registry: {}, head: { title: "", metatags: [] } };
if (router) {
await router.replace(pathname.split('/' + baseDirectoryName)[1]);
await router.isReady();
}
const rendered = await renderToString(app, ssrContext);
const content = utf8Decoder.decode(await Deno.readFile(siteTemplate))
.replace(``, rendered)
.replace(
``,
appHeaderScript({ ssrContext, entryPath: clientEntry }),
);
response = new Response(content, { headers: { "Content-Type": "text/html" } });
}
}
} else {
response = new Response("Only GET allowed.", { status: 500 });
}
if (response === null) {
response = new Response("Not found.", { status: 404 })
}
const timeEnd = new Date().getTime();
console.log(`Request ${ url?.pathname ?? 'malformed' }\tStatus ${ response.status }, Duration ${ timeEnd - timeStart }ms`);
return response;
});
Deno.addSignalListener("SIGINT", () => {
console.info("Shutting down (received SIGINT)");
Deno.exit();
});