Repository: pmcelhaney/counterfact
Pull request #314 (2022-11-22 01:38:04)

Rebuild landing page
Completely redid the landing page using [Rapidoc](https://rapidocweb.com/)

This was thrown together pretty quickly but I'm excited so I'm going to go ahead and publish another release. 


Patch:
diff --git a/.changeset/spicy-rocks-matter.md b/.changeset/spicy-rocks-matter.md
new file mode 100644
index 0000000..270d7cf
--- /dev/null
+++ b/.changeset/spicy-rocks-matter.md
@@ -0,0 +1,5 @@
+---
+"counterfact": minor
+---
+
+rebuilt (but very rough) landing page using rapidoc
diff --git a/package.json b/package.json
index 9dec4a8..2fc294d 100644
--- a/package.json
+++ b/package.json
@@ -62,6 +62,7 @@
     "chokidar": "^3.5.3",
     "commander": "^9.4.0",
     "fs-extra": "^10.1.0",
+    "handlebars": "^4.7.7",
     "js-yaml": "^4.1.0",
     "json-schema-faker": "^0.5.0-rcv.44",
     "json-schema-ref-parser": "^9.0.9",
diff --git a/src/client/index.html b/src/client/index.html
deleted file mode 100644
index 1d922c6..0000000
--- a/src/client/index.html
+++ /dev/null
@@ -1,43 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <title>Counterfact</title>
-    <meta name="description" content="Counterfact dashboard" />
-    <meta name="viewport" content="width=device-width, initial-scale=1" />
-    <style type="text/css">
-      body {
-        font-family: sans-serif;
-
-        margin: 20vh;
-        font-size: 3vh;
-        text-align: center;
-      }
-
-      ul {
-        list-style: none;
-        margin: 0;
-        line-height: 2em;
-      }
-    </style>
-  </head>
-  <body>
-    <h1>Counterfact is running!</h1>
-    <ul>
-      <li>
-        The generated code is at<br /><a href="vscode://file{{basePath}}"
-          >{{basePath}}</a
-        >
-      </li>
-      <li>
-        You can explore the API using
-        <a href="/counterfact/swagger">Swagger UI</a>
-      </li>
-      <li>
-        <a
-          href="https://github.com/pmcelhaney/counterfact/blob/main/docs/usage.md#generated-code-"
-          >How does this work?</a
-        >
-      </li>
-    </ul>
-  </body>
-</html>
diff --git a/src/client/index.html.hbs b/src/client/index.html.hbs
new file mode 100644
index 0000000..d57360d
--- /dev/null
+++ b/src/client/index.html.hbs
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>Counterfact</title>
+    <meta charset="utf-8">
+    <meta name="description" content="Counterfact dashboard" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
+
+
+     
+  </head>
+  <body>
+
+
+
+     <rapi-doc spec-url = "/counterfact/openapi"> 
+
+      <div slot="overview" style="background-color: #eee">
+        <p>Put an explanation of Counterfact here.</p>
+         <a
+            href="https://github.com/pmcelhaney/counterfact/blob/main/docs/usage.md#generated-code-"
+            >How does this work?</a
+          > 
+      </div>
+      {{#each routes as |route|}}
+        <a slot="get-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
+        <a slot="put-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
+        <a slot="post-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
+        <a slot="delete-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
+        <a slot="patch-{{escape_route route}}" href="vscode://file/{{../basePath}}/paths/{{.}}.ts">Edit in VSCode</a>
+
+      {{/each}}
+      </rapi-doc>
+
+
+
+  </body>
+</html>
diff --git a/src/server/registry.js b/src/server/registry.js
index 12fd476..333a4a8 100644
--- a/src/server/registry.js
+++ b/src/server/registry.js
@@ -9,6 +9,29 @@ function castParameters(parameters, parameterTypes) {
   return copy;
 }
 
+function maybe(flag, value) {
+  return flag ? [value] : [];
+}
+
+function stripBrackets(string) {
+  return string.replaceAll(/\{|\}/gu, "");
+}
+
+function routesForNode(node) {
+  if (!node.children) {
+    return [];
+  }
+
+  return Object.entries(node.children)
+    .flatMap(([segment, child]) => [
+      ...maybe(child.module, `/${segment}`),
+      ...routesForNode(child).map((route) => `/${segment}${route}`),
+    ])
+    .sort((segment1, segment2) =>
+      stripBrackets(segment1).localeCompare(stripBrackets(segment2))
+    );
+}
+
 export class Registry {
   modules = {};
 
@@ -16,6 +39,10 @@ export class Registry {
     children: {},
   };
 
+  get routes() {
+    return routesForNode(this.moduleTree);
+  }
+
   add(url, module) {
     let node = this.moduleTree;
 
diff --git a/src/server/start.js b/src/server/start.js
index bcc71b4..2b991a5 100644
--- a/src/server/start.js
+++ b/src/server/start.js
@@ -4,6 +4,7 @@ import yaml from "js-yaml";
 import Koa from "koa";
 import bodyParser from "koa-bodyparser";
 import { koaSwagger } from "koa2-swagger-ui";
+import Handlebars from "handlebars";
 
 import { readFile } from "../util/read-file.js";
 
@@ -14,7 +15,11 @@ const __dirname = nodePath.dirname(new URL(import.meta.url).pathname);
 
 const DEFAULT_PORT = 3100;
 
-function swaggerUi(app, openApiPath, port) {
+Handlebars.registerHelper("escape_route", (route) =>
+  route.replace(/[^\w/]/gu, "-")
+);
+
+function swaggerUi(app, openApiPath, url) {
   app.use(async (ctx, next) => {
     if (ctx.URL.pathname === "/counterfact/openapi") {
       const openApiDocument = await yaml.load(await readFile(openApiPath));
@@ -23,11 +28,11 @@ function swaggerUi(app, openApiPath, port) {
 
       openApiDocument.servers.unshift({
         description: "Counterfact",
-        url: `//localhost:${port}`,
+        url,
       });
 
       // OpenApi 2 support:
-      openApiDocument.host = `//localhost:${port}`;
+      openApiDocument.host = url;
 
       // eslint-disable-next-line require-atomic-updates
       ctx.body = yaml.dump(openApiDocument);
@@ -50,25 +55,9 @@ function swaggerUi(app, openApiPath, port) {
   );
 }
 
-export async function landingPageBody(basePath) {
-  const body = await readFile(nodePath.join(__dirname, "../client/index.html"));
-
-  return body.replaceAll("{{basePath}}", basePath);
-}
-
-export function landingPage(app, basePath) {
-  app.use(async (ctx, next) => {
-    if (ctx.URL.pathname === "/counterfact") {
-      // eslint-disable-next-line require-atomic-updates
-      ctx.body = await landingPageBody(basePath);
-
-      return;
-    }
-
-    // eslint-disable-next-line node/callback-return
-    await next();
-  });
-}
+export const landingPageTemplate = Handlebars.compile(
+  await readFile(nodePath.join(__dirname, "../client/index.html.hbs"))
+);
 
 export async function start({
   basePath = process.cwd(),
@@ -78,18 +67,32 @@ export async function start({
 }) {
   const app = new Koa();
 
+  const { koaMiddleware, contextRegistry, registry } = await counterfact(
+    basePath,
+    openApiPath
+  );
+
   if (includeSwaggerUi) {
-    swaggerUi(app, openApiPath, port);
+    swaggerUi(app, openApiPath, `//localhost:${port}`);
   }
 
-  landingPage(app, basePath);
+  app.use(async (ctx, next) => {
+    if (ctx.URL.pathname === "/counterfact") {
+      // eslint-disable-next-line require-atomic-updates
+      ctx.body = await landingPageTemplate({
+        basePath,
+        routes: registry.routes,
+        methods: ["get", "post", "put", "delete", "patch"],
+      });
 
-  app.use(bodyParser());
+      return;
+    }
 
-  const { koaMiddleware, contextRegistry } = await counterfact(
-    basePath,
-    openApiPath
-  );
+    // eslint-disable-next-line node/callback-return
+    await next();
+  });
+
+  app.use(bodyParser());
 
   app.use(koaMiddleware);
 


Test patch:
diff --git a/test/server/registry.test.js b/test/server/registry.test.js
index 3e9cd65..b785328 100644
--- a/test/server/registry.test.js
+++ b/test/server/registry.test.js
@@ -1,6 +1,6 @@
 import { Registry } from "../../src/server/registry.js";
 
-describe("a scripted server", () => {
+describe("a registry", () => {
   it("knows if a handler exists for a request method at a path", () => {
     const registry = new Registry();
 
@@ -102,4 +102,30 @@ describe("a scripted server", () => {
       body: "page 2 of alice's friends in acme",
     });
   });
+
+  it("lists all of the routes", () => {
+    const registry = new Registry();
+
+    registry.add("/a", {});
+    registry.add("/b", {});
+    registry.add("/c", {});
+    registry.add("/c/d", {});
+    registry.add("/c/d/e/f/g", {});
+    registry.add("/a/in/order", {});
+    registry.add("/c/a", {});
+    registry.add("/c/{b}", {});
+    registry.add("/c/c", {});
+
+    expect(registry.routes).toStrictEqual([
+      "/a",
+      "/a/in/order",
+      "/b",
+      "/c",
+      "/c/a",
+      "/c/{b}",
+      "/c/c",
+      "/c/d",
+      "/c/d/e/f/g",
+    ]);
+  });
 });
diff --git a/test/server/start.test.js b/test/server/start.test.js
index 6760f4e..e84ab5e 100644
--- a/test/server/start.test.js
+++ b/test/server/start.test.js
@@ -1,10 +1,11 @@
-import { landingPageBody } from "../../src/server/start.js";
+import { landingPageTemplate } from "../../src/server/start.js";
+
+// not great but better than nothing
 
 describe("start", () => {
   it("renders the landing page", async () => {
-    const basePath = "/home/user/counterfact";
-    const result = await landingPageBody(basePath);
+    const result = await landingPageTemplate({ routes: ["/hello/world"] });
 
-    expect(result).toContain(basePath);
+    expect(result).toContain("/hello/world");
   });
 });