djledda.de main
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

168 строки
6.0 KiB

  1. import { serveFile } from "jsr:@std/http/file-server";
  2. import { type App, toValue } from "vue";
  3. import { type Router } from "vue-router";
  4. import { renderToString } from "vue/server-renderer";
  5. import transpileResponse from "./transpile.ts";
  6. import { DOMParser } from "jsr:@b-fuze/deno-dom";
  7. import { join } from "jsr:@std/path/join";
  8. import { exists } from "jsr:@std/fs";
  9. import { type DJSSRContext } from "@/useDJSSRContext.ts";
  10. import { type DJAPIResult, type DJAPIResultMap } from "@/api.ts";
  11. const utf8Decoder = new TextDecoder("utf-8");
  12. const parser = new DOMParser();
  13. function appHeaderScript(params: { appstate: Record<string, unknown>; entryPath: string }) {
  14. return `<script type="importmap">
  15. {
  16. "imports": {
  17. "vue": "/deps/vue/dist/vue.esm-browser.prod.js",
  18. "vue-router": "/deps/vue-router/dist/vue-router.esm-browser.js",
  19. "vue/jsx-runtime": "/deps/vue/jsx-runtime/index.mjs",
  20. "@vue/devtools-api": "/deps/@vue/devtools-api/lib/esm/index.js",
  21. "@/": "/app/"
  22. }
  23. }
  24. </script>
  25. <script type="module">
  26. window.appstate = ${JSON.stringify(params.appstate)};
  27. import('${params.entryPath}');
  28. </script>`;
  29. }
  30. async function* siteEntries(path: string): AsyncGenerator<string> {
  31. for await (const dirEnt of Deno.readDir(path)) {
  32. if (dirEnt.isDirectory) {
  33. yield* siteEntries(join(path, dirEnt.name));
  34. } else if (dirEnt.name === "index_template.html") {
  35. yield path.split("/")[1] ?? "";
  36. }
  37. }
  38. }
  39. const publicFiles = siteEntries("public");
  40. const sites: string[] = [];
  41. for await (const path of publicFiles) {
  42. sites.push(path);
  43. }
  44. function getAPIResponse(apiReq: Request): Response {
  45. let jsonResponse: DJAPIResult | { error: string } | null = null;
  46. let status = 200;
  47. const pathname = URL.parse(apiReq.url)?.pathname;
  48. if (!pathname) {
  49. jsonResponse = { error: "Invalid Route" };
  50. status = 400;
  51. }
  52. if (!jsonResponse && pathname) {
  53. const apiPath = pathname.split("/api")[1];
  54. if (apiPath === "/rp-articles") {
  55. jsonResponse = [
  56. { name: "Koffein: ein vitamin-ähnlicher Nährstoff, oder Adaptogen", slug: "caffeine" },
  57. {
  58. name: "TSH, Temperatur, Puls, und andere Indikatoren bei einer Schilddrüsenunterfunktion",
  59. slug: "hypothyroidism",
  60. },
  61. ] satisfies DJAPIResultMap["/rp-articles"];
  62. }
  63. }
  64. const headers = new Headers();
  65. headers.set("Content-Type", "application/json");
  66. return new Response(JSON.stringify(jsonResponse), {
  67. status,
  68. headers,
  69. });
  70. }
  71. Deno.serve({
  72. port: 8080,
  73. hostname: "0.0.0.0",
  74. onListen({ port, hostname }) {
  75. console.log(`Listening on port http://${hostname}:${port}/`);
  76. },
  77. }, async (req, _conn) => {
  78. let response: Response | null = null;
  79. if (req.method === "GET") {
  80. const pathname = URL.parse(req.url)?.pathname ?? "/";
  81. if (pathname.startsWith("/api/")) {
  82. response = getAPIResponse(req);
  83. }
  84. // Public/static files
  85. if (!response) {
  86. let filepath = join(".", "public", pathname);
  87. if (filepath.endsWith("/")) {
  88. filepath = join(filepath, "index.html");
  89. }
  90. if (await exists(filepath, { isFile: true })) {
  91. response = await serveFile(req, filepath);
  92. }
  93. }
  94. // NPM Vendor deps
  95. if (response === null && pathname.startsWith("/deps")) {
  96. response = await serveFile(req, `node_modules/${pathname.split("/deps")[1]}`);
  97. }
  98. // Transpile Code
  99. if (
  100. response === null &&
  101. pathname.startsWith("/app") &&
  102. (pathname.endsWith(".ts") || pathname.endsWith(".tsx")) &&
  103. !pathname.endsWith("server.ts")
  104. ) {
  105. response = await serveFile(req, "./" + pathname);
  106. response = await transpileResponse(response, req.url, pathname);
  107. }
  108. // SSR
  109. if (response === null) {
  110. const baseDirectoryName = pathname.split("/")[1] ?? "";
  111. if (sites.includes(baseDirectoryName)) {
  112. const appLocation = baseDirectoryName === "" ? "home" : baseDirectoryName;
  113. const siteTemplate = join("public", baseDirectoryName, "index_template.html");
  114. const siteEntry = join("app", appLocation, "server.ts");
  115. const clientEntry = join("@", appLocation, "client.ts");
  116. const { app, router } = (await import("./" + siteEntry)).default() as {
  117. app: App;
  118. router: Router | null;
  119. };
  120. app.provide("dom-parse", (innerHTML: string) => {
  121. return parser.parseFromString(innerHTML, "text/html").documentElement;
  122. });
  123. const ssrContext: DJSSRContext = { registry: {}, head: { title: "" } };
  124. if (router) {
  125. await router.replace(pathname.split("/generative-energy")[1]);
  126. await router.isReady();
  127. }
  128. const rendered = await renderToString(app, ssrContext);
  129. const content = utf8Decoder.decode(await Deno.readFile(siteTemplate))
  130. .replace(`<!-- SSR OUTLET -->`, rendered)
  131. .replaceAll("%TITLE%", toValue(ssrContext.head?.title) ?? "Site")
  132. .replace(
  133. `<!-- SSR HEAD OUTLET -->`,
  134. appHeaderScript({ appstate: ssrContext.registry, entryPath: clientEntry }),
  135. );
  136. response = new Response(content, { headers: { "Content-Type": "text/html" } });
  137. }
  138. }
  139. } else {
  140. response = new Response("Only GET allowed.", { status: 500 });
  141. }
  142. return response ?? new Response("Not found.", { status: 404 });
  143. });
  144. Deno.addSignalListener("SIGINT", () => {
  145. console.info("Shutting down (received SIGINT)");
  146. Deno.exit();
  147. });