|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- 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 `
- <title>${ toValue(params.ssrContext.head.title) }</title>
- ${ toValue(params.ssrContext.head.metatags).map(_ => `<meta name="${ _.name }" content="${ _.content }">`).join('\n\t') }
- <script type="importmap">
- {
- "imports": {
- "vue": "/deps/vue/dist/vue.esm-browser.prod.js",
- "vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js",
- "vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs",
- "@vue/devtools-api": "/app/devtools-shim.ts",
- "@/": "/app/"
- }
- }
- </script>
- <style>
- ${ Object.values(params.ssrContext.styles).join('\n') }
- </style>
- <script type="module">
- window.appstate = ${JSON.stringify(params.ssrContext.registry)};
- import('${params.entryPath}');
- </script>`;
- }
-
- async function* siteEntries(path: string): AsyncGenerator<string> {
- 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<Response> {
- 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<HTMLMetaElement>;
- 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(`<!-- SSR OUTLET -->`, rendered)
- .replace(
- `<!-- SSR HEAD OUTLET -->`,
- 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();
- });
|