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(); });