mirror of
https://github.com/Drop-OSS/drop-api-autogen.git
synced 2026-01-30 20:55:17 +01:00
feat: improvements and initial UI design
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtWelcome />
|
||||
</div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
|
||||
77
app/assets/css/core.scss
Normal file
77
app/assets/css/core.scss
Normal file
@@ -0,0 +1,77 @@
|
||||
$motiva: (
|
||||
("MotivaSansThin.ttf", "ttf", 100, normal),
|
||||
("MotivaSansLight.woff.ttf", "woff", 300, normal),
|
||||
("MotivaSansRegular.woff.ttf", "woff", 400, normal),
|
||||
("MotivaSansMedium.woff.ttf", "woff", 500, normal),
|
||||
("MotivaSansBold.woff.ttf", "woff", 600, normal),
|
||||
("MotivaSansExtraBold.ttf", "woff", 700, normal),
|
||||
("MotivaSansBlack.woff.ttf", "woff", 900, normal)
|
||||
);
|
||||
|
||||
$helvetica: (
|
||||
("Helvetica.woff", "woff", 400, normal),
|
||||
("Helvetica-Oblique.woff", "woff", 400, italic),
|
||||
("Helvetica-Bold.woff", "woff", 600, normal),
|
||||
("Helvetica-BoldOblique.woff", "woff", 600, italic),
|
||||
("helvetica-light-587ebe5a59211.woff2", "woff2", 300, normal)
|
||||
);
|
||||
|
||||
@each $file, $format, $weight, $style in $motiva {
|
||||
@font-face {
|
||||
font-family: "Motiva Sans";
|
||||
src: url("/fonts/motiva/#{$file}") format($format);
|
||||
font-weight: $weight;
|
||||
font-style: $style;
|
||||
}
|
||||
}
|
||||
|
||||
@each $file, $format, $weight, $style in $helvetica {
|
||||
@font-face {
|
||||
font-family: "Helvetica";
|
||||
src: url("/fonts/helvetica/#{$file}") format($format);
|
||||
font-weight: $weight;
|
||||
font-style: $style;
|
||||
}
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/fonts/inter/InterVariable.ttf");
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inter";
|
||||
src: url("/fonts/inter/InterVariable-Italic.ttf");
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.carousel__icon {
|
||||
color: #f4f4f5;
|
||||
}
|
||||
.carousel__pagination-button::after {
|
||||
background-color: #3f3f46;
|
||||
border-radius: 999999px;
|
||||
}
|
||||
.carousel__pagination-button:hover::after {
|
||||
background-color: #27272a;
|
||||
border-radius: 999999px;
|
||||
}
|
||||
.carousel__pagination-button--active::after {
|
||||
background-color: #a1a1aa;
|
||||
}
|
||||
.carousel__pagination-button--active:hover::after {
|
||||
background-color: #d4d4d8;
|
||||
}
|
||||
|
||||
.store-carousel > .carousel__viewport {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
|
||||
html {
|
||||
background-color: oklch(0.21 0.006 285.885);
|
||||
}
|
||||
13
app/assets/css/tailwind.css
Normal file
13
app/assets/css/tailwind.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "@tailwindcss/forms";
|
||||
@config "../../../tailwind.config.js";
|
||||
|
||||
@layer base {
|
||||
input[type="number"]::-webkit-outer-spin-button,
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"] {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: textfield !important;
|
||||
}
|
||||
}
|
||||
8
app/components/Logo.vue
Normal file
8
app/components/Logo.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg aria-label="Drop Logo" class="text-blue-400" viewBox="0 0 24 24" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 13.5C4 11.0008 5.38798 8.76189 7.00766 7C8.43926 5.44272 10.0519 4.25811 11.0471 3.5959C11.6287 3.20893 12.3713 3.20893 12.9529 3.5959C13.9481 4.25811 15.5607 5.44272 16.9923 7C18.612 8.76189 20 11.0008 20 13.5C20 17.9183 16.4183 21.5 12 21.5C7.58172 21.5 4 17.9183 4 13.5Z"
|
||||
stroke="currentColor" stroke-width="2" />
|
||||
</svg>
|
||||
</template>
|
||||
69
app/components/Navigation.vue
Normal file
69
app/components/Navigation.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<ul class="space-y-3">
|
||||
<li v-for="[path, methods] in collapsedAPINav.entries()">
|
||||
<span class="font-display text-zinc-100 font-semibold">{{ path }}</span>
|
||||
<ul class="mt-1 space-y-1">
|
||||
<li v-for="method in methods">
|
||||
<NuxtLink
|
||||
class="transition hover:bg-zinc-900 px-1 py-0.5 rounded ml-1 text-zinc-400 grid grid-cols-4 gap-x-2"
|
||||
:href="method.path">
|
||||
|
||||
<span
|
||||
:class="[apiMethodColours[method.method], 'text-center text-xs font-bold ring-1 rounded-full px-1 py-0.5']">{{
|
||||
method.method }}</span>
|
||||
<div class="col-span-3 text-sm relative whitespace-nowrap overflow-hidden"><span
|
||||
class="absolute right-0">{{ method.methodlessName }}</span></div>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content';
|
||||
|
||||
const rawAPINav = await queryCollectionNavigation("docs");
|
||||
const apiNavRoot = rawAPINav.at(0);
|
||||
if (!apiNavRoot) throw createError({ statusCode: 500, statusMessage: "Failed to fetch API docs navigation", fatal: true });
|
||||
|
||||
const collapsedAPINav = ref<Map<string, Array<ContentNavigationItem & { method: string, methodlessName: string }>>>(new Map());
|
||||
const apiMethods = ["GET", "POST", "DELETE", "PATCH"];
|
||||
|
||||
const apiMethodColours: { [key: string]: string } = {
|
||||
"GET": "text-green-500 bg-green-500/10",
|
||||
"POST": "text-orange-500 bg-orange-500/10",
|
||||
"PATCH": "text-yellow-500 bg-yellow-500/10",
|
||||
"DELETE": "text-red-500 bg-red-500/10",
|
||||
"WS": "text-blue-500 bg-blue-500/10",
|
||||
}
|
||||
|
||||
function recursivelyCollapseAPINav(nav: ContentNavigationItem, parent?: ContentNavigationItem) {
|
||||
const potentialMethod = nav.title.split(" ").at(0);
|
||||
if (!potentialMethod || !apiMethods.includes(potentialMethod.toUpperCase()) || !parent) {
|
||||
if (nav.children) {
|
||||
for (const child of nav.children)
|
||||
recursivelyCollapseAPINav(child, nav);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const methodlessName = nav.title.slice(potentialMethod.length).trim();
|
||||
|
||||
const newValue = collapsedAPINav.value.get(parent.path) ?? [];
|
||||
newValue.push({ ...nav, method: potentialMethod, methodlessName })
|
||||
collapsedAPINav.value.set(parent.path, newValue);
|
||||
}
|
||||
|
||||
recursivelyCollapseAPINav(apiNavRoot)
|
||||
|
||||
collapsedAPINav.value = new Map([...collapsedAPINav.value].sort((a, b) => a[0].localeCompare(b[0])).map((v) => {
|
||||
v[1].sort((a, b) => {
|
||||
const methodSort = a.method.length - b.method.length;
|
||||
if (methodSort == 0) return a.methodlessName.length - b.methodlessName.length;
|
||||
return methodSort;
|
||||
});
|
||||
return v;
|
||||
}))
|
||||
|
||||
</script>
|
||||
29
app/components/Wordmark.vue
Normal file
29
app/components/Wordmark.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="inline-flex justify-center items-center gap-x-1 -mb-1 relative">
|
||||
<svg aria-hidden="true" viewBox="0 0 418 42" class="absolute inset-0 h-full w-full fill-blue-300/30 scale-75"
|
||||
preserveAspectRatio="none">
|
||||
<path
|
||||
d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
|
||||
</svg>
|
||||
<Logo aria-hidden="true" class="h-6" />
|
||||
<span class="text-blue-400 font-display font-bold text-xl uppercase">
|
||||
Drop
|
||||
</span>
|
||||
<span class="-mb-0.5 ml-1 text-zinc-100 font-bold text-xl uppercase">
|
||||
API
|
||||
</span>
|
||||
<span class="blink h-[3px] w-3 rounded-full bg-blue-400 absolute translate-x-full right-[-3px] bottom-[1px]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.blink {
|
||||
animation: blink 1.5s step-start 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0.0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
app/layouts/default.vue
Normal file
24
app/layouts/default.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="bg-zinc-950 h-screen w-full">
|
||||
<div class="h-full lg:ml-72 xl:ml-80">
|
||||
<div class="contents lg:pointer-events-none lg:fixed lg:inset-0 lg:z-40 lg:flex">
|
||||
<div
|
||||
class="contents lg:pointer-events-auto lg:block lg:w-72 lg:overflow-y-auto lg:border-r lg:border-zinc-100/10 lg:px-6 lg:pt-4 lg:pb-8 xl:w-80">
|
||||
<div class="hidden lg:flex">
|
||||
<NuxtLink href="/" aria-label="Home">
|
||||
<Wordmark class="h-6" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<Header />
|
||||
<Navigation class="hidden lg:mt-10 lg:block" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative overflow-y-auto flex h-full flex-col px-4 pt-14 sm:px-6 lg:px-8">
|
||||
<main class="flex-auto">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
30
app/pages/api/[...slug].vue
Normal file
30
app/pages/api/[...slug].vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () => queryCollection('docs').path(route.path).first())
|
||||
|
||||
page.value?.body.toc
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.title,
|
||||
description: page.value?.description
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="page" class="flex flex-row gap-8">
|
||||
<ContentRenderer class="grow prose prose-invert prose-blue max-w-none" :value="page" />
|
||||
<div class="sticky top-0 w-96 h-min" v-if="page.body.toc">
|
||||
<h1 class="text-zinc-400">On this page</h1>
|
||||
<ul class="mt-1 ">
|
||||
<li v-for="link in page.body.toc.links">
|
||||
<a :href="`#${link.id}`" class="transition text-zinc-500 hover:text-zinc-200 text-sm">
|
||||
{{ link.text }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>Page not found</div>
|
||||
|
||||
</template>
|
||||
15
app/pages/guides/[...slug].vue
Normal file
15
app/pages/guides/[...slug].vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute();
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () => queryCollection('guides').path(route.path).first())
|
||||
|
||||
useSeoMeta({
|
||||
title: page.value?.title,
|
||||
description: page.value?.description
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContentRenderer v-if="page" :value="page" />
|
||||
<div v-else>Page not found</div>
|
||||
</template>
|
||||
3
app/pages/index.vue
Normal file
3
app/pages/index.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<h1>Hello world</h1>
|
||||
</template>
|
||||
22
content.config.ts
Normal file
22
content.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineContentConfig, defineCollection } from "@nuxt/content";
|
||||
|
||||
export default defineContentConfig({
|
||||
collections: {
|
||||
docs: defineCollection({
|
||||
type: "page",
|
||||
source: {
|
||||
include: "**/*.md",
|
||||
prefix: "api",
|
||||
cwd: "./docs",
|
||||
},
|
||||
}),
|
||||
guides: defineCollection({
|
||||
type: "page",
|
||||
source: {
|
||||
include: "**/*.md",
|
||||
prefix: "guides",
|
||||
cwd: "./guides",
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
2
drop
2
drop
Submodule drop updated: 7af29ef0eb...29fdfcbdd4
1
guides/creating-token.md
Normal file
1
guides/creating-token.md
Normal file
@@ -0,0 +1 @@
|
||||
# How to create an API token
|
||||
@@ -1,12 +1,21 @@
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2025-07-15',
|
||||
compatibilityDate: "2025-07-15",
|
||||
devtools: { enabled: true },
|
||||
|
||||
modules: ["@nuxt/content"],
|
||||
|
||||
vite: { plugins: [tailwindcss()] },
|
||||
|
||||
css: ["~/assets/css/core.scss", "~/assets/css/tailwind.css"],
|
||||
|
||||
hooks: {
|
||||
"build:before": async () => {
|
||||
// Dynamic import so I can ignore errors. (I'm lazy)
|
||||
const build = (await import("./tools/index")).default;
|
||||
await build();
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,10 +10,19 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/content": "^3.6.3",
|
||||
"@tailwindcss/forms": "^0.5.10",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"nuxt": "^4.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"typescript": "^5.9.2",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sass-embedded": "^1.90.0"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
BIN
public/fonts/helvetica/Helvetica-Bold.woff
Normal file
BIN
public/fonts/helvetica/Helvetica-Bold.woff
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/Helvetica-BoldOblique.woff
Normal file
BIN
public/fonts/helvetica/Helvetica-BoldOblique.woff
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/Helvetica-Oblique.woff
Normal file
BIN
public/fonts/helvetica/Helvetica-Oblique.woff
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/Helvetica.woff
Normal file
BIN
public/fonts/helvetica/Helvetica.woff
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/helvetica-compressed-5871d14b6903a.woff
Normal file
BIN
public/fonts/helvetica/helvetica-compressed-5871d14b6903a.woff
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/helvetica-light-587ebe5a59211.woff
Normal file
BIN
public/fonts/helvetica/helvetica-light-587ebe5a59211.woff
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/helvetica-light-587ebe5a59211.woff2
Normal file
BIN
public/fonts/helvetica/helvetica-light-587ebe5a59211.woff2
Normal file
Binary file not shown.
BIN
public/fonts/helvetica/helvetica-rounded-bold-5871d05ead8de.woff
Normal file
BIN
public/fonts/helvetica/helvetica-rounded-bold-5871d05ead8de.woff
Normal file
Binary file not shown.
BIN
public/fonts/inter/InterVariable-Italic.ttf
Normal file
BIN
public/fonts/inter/InterVariable-Italic.ttf
Normal file
Binary file not shown.
BIN
public/fonts/inter/InterVariable.ttf
Normal file
BIN
public/fonts/inter/InterVariable.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansBlack.woff.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansBlack.woff.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansBold.woff.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansBold.woff.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansExtraBold.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansExtraBold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansLight.woff.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansLight.woff.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansMedium.woff.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansMedium.woff.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansRegular.woff.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansRegular.woff.ttf
Normal file
Binary file not shown.
BIN
public/fonts/motiva/MotivaSansThin.ttf
Normal file
BIN
public/fonts/motiva/MotivaSansThin.ttf
Normal file
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
24
tailwind.config.js
Normal file
24
tailwind.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./app/components/**/*.{js,vue,ts}",
|
||||
"./app/layouts/**/*.vue",
|
||||
"./app/pages/**/*.vue",
|
||||
"./app/plugins/**/*.{js,ts}",
|
||||
"./app/app.vue",
|
||||
"./app/error.vue",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["Inter"],
|
||||
display: ["Motiva Sans"],
|
||||
},
|
||||
colors: {
|
||||
zinc: {
|
||||
925: "#111112",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,31 +1,51 @@
|
||||
import { Route, RouteMetadata } from ".";
|
||||
import * as ts from "typescript";
|
||||
import type { Route, RouteMetadata } from ".";
|
||||
import prettier from "prettier";
|
||||
|
||||
function encouragePrettierLinebreaks(rawType: string) {
|
||||
return rawType.startsWith("{")
|
||||
? rawType.slice(0, 1) + "\n" + rawType.slice(1)
|
||||
: rawType;
|
||||
}
|
||||
|
||||
export default async function generateDocsPage(
|
||||
route: Route,
|
||||
metadata: RouteMetadata
|
||||
) {
|
||||
// ===== Body =====
|
||||
let bodyText = "";
|
||||
if (metadata.body) {
|
||||
bodyText = `
|
||||
## Request Body
|
||||
|
||||
${metadata.bodyComment ?? ""}
|
||||
|
||||
Request type: \`application/json\`. Body type (TypeScript definition):
|
||||
|
||||
|
||||
`.trimStart();
|
||||
bodyText += "```typescript\n";
|
||||
|
||||
const rawType = metadata.checker.typeToString(metadata.body);
|
||||
const newLinedType = rawType.slice(0, 1) + "\n" + rawType.slice(1);
|
||||
const rawType = metadata.checker.typeToString(
|
||||
metadata.body,
|
||||
undefined,
|
||||
ts.TypeFormatFlags.NoTruncation
|
||||
);
|
||||
const newLinedType = encouragePrettierLinebreaks(rawType);
|
||||
const pretty = await prettier.format(`type Body = ${newLinedType}`, {
|
||||
parser: "typescript",
|
||||
});
|
||||
|
||||
bodyText += pretty;
|
||||
bodyText += "\n```";
|
||||
} else if (metadata.bodyComment) {
|
||||
bodyText = `
|
||||
## Request Body
|
||||
|
||||
${metadata.bodyComment ?? ""}`;
|
||||
}
|
||||
|
||||
// ===== Route Params =====
|
||||
let paramTable = "";
|
||||
if (metadata.routeParams) {
|
||||
paramTable =
|
||||
@@ -35,6 +55,18 @@ Request type: \`application/json\`. Body type (TypeScript definition):
|
||||
}
|
||||
}
|
||||
|
||||
let queryTable = "";
|
||||
if (metadata.query) {
|
||||
const queryProperties = metadata.query.getProperties();
|
||||
if (queryProperties.length > 0) {
|
||||
queryTable = "## Query Parameters\n\n| Name | Type |\n| ---- | ---- |\n";
|
||||
for (const property of queryProperties) {
|
||||
queryTable += `| ${property.escapedName} | string |\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== ACLs =====
|
||||
let routeAuthentication = "";
|
||||
if (metadata.acls && metadata.acls.length > 0) {
|
||||
routeAuthentication = `
|
||||
@@ -50,12 +82,46 @@ ${metadata.acls.map((e) => ` - \`${e}\``).join("\n")}
|
||||
routeAuthentication = `You can access this route with any \`${metadata.aclMode}\` token.`;
|
||||
}
|
||||
|
||||
// ===== Client Warning =====
|
||||
let clientWarning = "";
|
||||
if (metadata.clientRoute) {
|
||||
clientWarning =
|
||||
"**This is a client route. It is not accessible through API tokens.**";
|
||||
}
|
||||
|
||||
// ===== Response ======
|
||||
let responseText = "";
|
||||
if (metadata.response) {
|
||||
responseText = `
|
||||
## Response Body
|
||||
|
||||
${metadata.responseComment ?? ""}
|
||||
|
||||
${metadata.responseTag ? `This endpoint returns '${metadata.responseTag}'` : ""}
|
||||
|
||||
Response definition:
|
||||
|
||||
`.trimStart();
|
||||
responseText += "```typescript\n";
|
||||
|
||||
const rawType = metadata.checker.typeToString(
|
||||
metadata.response,
|
||||
undefined,
|
||||
ts.TypeFormatFlags.NoTruncation
|
||||
);
|
||||
const newLinedType = encouragePrettierLinebreaks(rawType);
|
||||
const pretty = await prettier.format(`type Response = ${newLinedType}`, {
|
||||
parser: "typescript",
|
||||
});
|
||||
|
||||
responseText += pretty;
|
||||
responseText += "\n```";
|
||||
} else if (metadata.responseComment) {
|
||||
responseText = `
|
||||
## Response Body
|
||||
${metadata.responseComment}`.trim();
|
||||
}
|
||||
|
||||
const rawText = `
|
||||
# ${route.method} \`${route.urlPath}\`
|
||||
|
||||
@@ -66,10 +132,14 @@ ${routeAuthentication}
|
||||
|
||||
${clientWarning}
|
||||
|
||||
${queryTable}
|
||||
|
||||
${paramTable}
|
||||
|
||||
${bodyText}
|
||||
|
||||
${responseText}
|
||||
|
||||
`;
|
||||
|
||||
// Just for my sanity
|
||||
|
||||
@@ -67,7 +67,15 @@ async function transformRoutes(base: string, routes: string[]) {
|
||||
|
||||
export interface RouteMetadata {
|
||||
checker: ts.TypeChecker;
|
||||
|
||||
body?: ts.Type;
|
||||
bodyComment?: string;
|
||||
|
||||
query?: ts.Type;
|
||||
|
||||
response?: ts.Type;
|
||||
responseTag?: string;
|
||||
responseComment?: string;
|
||||
|
||||
routeDescription?: string;
|
||||
routeParams?: { [key: string]: { type: string; comment: string } };
|
||||
@@ -118,36 +126,49 @@ async function generateDocs(
|
||||
const isClient = eventHandlerName.toLowerCase().includes("client");
|
||||
|
||||
const metadata: RouteMetadata = { checker, clientRoute: isClient };
|
||||
|
||||
// Extract the body type from the generic args
|
||||
// Complicated and messy
|
||||
if (eventHandler.typeArguments) {
|
||||
|
||||
// Only run on client/normal handlers
|
||||
if (!eventHandlerName.toLowerCase().includes("websocket")) {
|
||||
const resolvedSignature = checker.getResolvedSignature(eventHandler);
|
||||
if (!resolvedSignature)
|
||||
throw "Type arguments but no resolved signature? Somehow an invalid node?";
|
||||
throw "No resolved signature? Somehow an invalid node?";
|
||||
|
||||
// Get handler (function) type
|
||||
const handlerParam = resolvedSignature.getParameters().at(0)!;
|
||||
const handlerType = checker.getTypeOfSymbolAtLocation(
|
||||
let handlerType = checker.getTypeOfSymbolAtLocation(
|
||||
handlerParam,
|
||||
handlerParam.declarations!.at(0)!
|
||||
);
|
||||
if (!handlerType.isUnion())
|
||||
throw "Weird defineEventHandler result - did Nitro change something interally?";
|
||||
if (handlerType.isUnion()) handlerType = handlerType.types.at(0)!;
|
||||
|
||||
const eventHandlerRequestType = handlerType.types.at(0)!;
|
||||
const typeArguments = eventHandlerRequestType[
|
||||
const typeArguments = (handlerType as any)[
|
||||
"resolvedTypeArguments"
|
||||
] as ts.Type[];
|
||||
|
||||
const requestConfigurationType = typeArguments.at(0)!;
|
||||
if (typeArguments) {
|
||||
const requestConfigurationType = typeArguments.at(0)!;
|
||||
|
||||
const properties = requestConfigurationType.getProperties();
|
||||
for (const property of properties) {
|
||||
metadata[property.escapedName!] = checker.getTypeOfSymbolAtLocation(
|
||||
property,
|
||||
property.declarations!.at(0)!
|
||||
);
|
||||
const properties = requestConfigurationType.getProperties();
|
||||
for (const property of properties) {
|
||||
const type = checker.getTypeOfSymbolAtLocation(
|
||||
property,
|
||||
property.declarations!.at(0)!
|
||||
);
|
||||
|
||||
if (type.flags & ts.TypeFlags.Any) continue;
|
||||
(metadata as any)[property.escapedName!] = type;
|
||||
}
|
||||
|
||||
const responseType = typeArguments.at(1)!;
|
||||
if (!(responseType.flags & ts.TypeFlags.Any))
|
||||
metadata.response = checker.getAwaitedType(responseType);
|
||||
}
|
||||
} else {
|
||||
// If it's websocket, change the "method"
|
||||
route.method = "WS";
|
||||
}
|
||||
|
||||
// A kinda inefficient search through every node in the AST
|
||||
@@ -191,19 +212,33 @@ async function generateDocs(
|
||||
// Extract and parse the JSDoc for additional metadata
|
||||
const jsDoc = ts.getJSDocCommentsAndTags(exportAssignment).at(0);
|
||||
if (jsDoc) {
|
||||
metadata.routeParams ??= {};
|
||||
|
||||
const description = jsDoc.comment;
|
||||
if (description) {
|
||||
metadata.routeDescription = description.toString();
|
||||
}
|
||||
|
||||
for (const tag of jsDoc.getChildren()) {
|
||||
if (!ts.isJSDocParameterTag(tag)) continue;
|
||||
const type = tag.typeExpression?.getText().slice(1, -1) ?? "string";
|
||||
const paramName = tag.name.getText();
|
||||
const comment = tag.comment?.toString() ?? "";
|
||||
metadata.routeParams[paramName] = { type, comment };
|
||||
if (ts.isJSDocParameterTag(tag)) {
|
||||
const type = tag.typeExpression?.getText().slice(1, -1) ?? "string";
|
||||
const paramName = tag.name.getText();
|
||||
const comment = tag.comment?.toString() ?? "";
|
||||
metadata.routeParams ??= {};
|
||||
metadata.routeParams[paramName] = { type, comment };
|
||||
}
|
||||
|
||||
if (ts.isJSDocReturnTag(tag)) {
|
||||
metadata.responseTag = tag.comment?.toString();
|
||||
}
|
||||
|
||||
if (ts.isJSDocUnknownTag(tag)) {
|
||||
if (tag.tagName.escapedText == "request") {
|
||||
// Custom request text
|
||||
metadata.bodyComment = tag.comment?.toString();
|
||||
}
|
||||
if (tag.tagName.escapedText == "response") {
|
||||
metadata.responseComment = tag.comment?.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user