Create an example app for OpenAPI

This commit is contained in:
fuma-nama
2024-08-06 19:24:22 +08:00
parent deae4dd86c
commit 480d211f42
30 changed files with 422 additions and 57 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'fumadocs-openapi': minor
---
Change output path logic
+2 -2
View File
@@ -13,8 +13,8 @@ apps/docs/shaders/*
apps/docs/content/docs/ui/museum/*.mdx
!apps/docs/content/docs/ui/museum/index.mdx
apps/docs/content/docs/unkey/**/*.mdx
!apps/docs/content/docs/unkey/index.mdx
examples/openapi/content/docs/(api)/**/*.mdx
!examples/openapi/content/docs/(api)/index.mdx
apps/docs/content/docs/ui/typescript.mdx
+1 -4
View File
@@ -1,8 +1,5 @@
out
content/docs/ui/museum/*.mdx
content/docs/ui/museum/**/*.mdx
!content/docs/ui/museum/index.mdx
content/docs/unkey/**/*.mdx
!content/docs/unkey/index.mdx
content/docs/ui/(integrations)/typescript.mdx
+1 -1
View File
@@ -94,7 +94,7 @@ function Item(
className="rounded-2xl border border-transparent p-6 shadow-lg transition-all hover:shadow-fd-primary/20"
style={{
backgroundImage:
'linear-gradient(to right bottom, hsl(var(--background)) 10%, hsl(var(--fd-accent)), hsl(var(--background)) 60%),' +
'linear-gradient(to right bottom, hsl(var(--background)) 10%, hsl(var(--accent)), hsl(var(--background)) 60%),' +
'linear-gradient(to right bottom, rgb(40,40,40) 10%, rgb(180,180,180), rgb(30,30,30) 60%)',
backgroundOrigin: 'border-box',
backgroundClip: 'padding-box, border-box',
-5
View File
@@ -1,5 +0,0 @@
---
title: Unkey
description: Experimenting OpenAPI
index: true
---
-4
View File
@@ -1,4 +0,0 @@
{
"title": "Unkey",
"root": true
}
+1 -14
View File
@@ -3,12 +3,6 @@ import * as Typescript from 'fumadocs-typescript';
import * as path from 'node:path';
import { rimrafSync } from 'rimraf';
rimrafSync('./content/docs/ui/unkey', {
filter(v) {
return !v.endsWith('index.mdx') && !v.endsWith('meta.json');
},
});
rimrafSync('./content/docs/ui/museum', {
filter(v) {
return !v.endsWith('index.mdx') && !v.endsWith('meta.json');
@@ -17,17 +11,10 @@ rimrafSync('./content/docs/ui/museum', {
void OpenAPI.generateFiles({
input: ['./museum.yaml'],
output: './content/docs/ui',
output: './content/docs/ui/museum',
per: 'operation',
});
void OpenAPI.generateFiles({
input: ['./unkey.json'],
output: './content/docs',
per: 'operation',
groupBy: 'tag',
});
const demoRegex = /^---type-table-demo---\r?\n(?<content>.+)\r?\n---end---$/gm;
void Typescript.generateFiles({
input: ['./content/docs/**/*.model.mdx'],
-9
View File
@@ -4,7 +4,6 @@ import {
LibraryIcon,
PaperclipIcon,
type LucideIcon,
KeyIcon,
} from 'lucide-react';
import mdx from '../../../packages/mdx/package.json';
import ui from '../../../packages/ui/package.json';
@@ -44,12 +43,4 @@ export const modes: Mode[] = [
version: mdx.version,
icon: PaperclipIcon,
},
{
param: 'unkey',
name: 'Unkey',
package: 'test',
description: 'Experimental OpenAPI',
version: '0',
icon: KeyIcon,
},
];
+1
View File
@@ -0,0 +1 @@
content/docs/(api)/**/*.mdx
+3
View File
@@ -0,0 +1,3 @@
# Unkey API Docs
This is an example API documentation based on Unkey's [API documentation](https://github.com/unkeyed/unkey), using Fumadocs OpenAPI.
+11
View File
@@ -0,0 +1,11 @@
import { getPages } from '@/app/source';
import { createSearchAPI } from 'fumadocs-core/search/server';
export const { GET } = createSearchAPI('advanced', {
indexes: getPages().map((page) => ({
title: page.data.title,
structuredData: page.data.exports.structuredData,
id: page.url,
url: page.url,
})),
});
@@ -0,0 +1,47 @@
import { getPage, getPages } from '@/app/source';
import type { Metadata } from 'next';
import { DocsPage, DocsBody } from 'fumadocs-ui/page';
import { notFound } from 'next/navigation';
export default async function Page({
params,
}: {
params: { slug?: string[] };
}) {
const page = getPage(params.slug);
if (!page) {
notFound();
}
const MDX = page.data.exports.default;
return (
<DocsPage toc={page.data.exports.toc} full={page.data.full}>
<h1 className="text-3xl font-bold text-fd-foreground sm:text-4xl">
{page.data.title}
</h1>
<p className="text-fd-muted-foreground">{page.data.description}</p>
<DocsBody>
<MDX />
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return getPages().map((page) => ({
slug: page.slugs,
}));
}
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
const page = getPage(params.slug);
if (page == null) notFound();
return {
title: page.data.title,
description: page.data.description,
} satisfies Metadata;
}
+17
View File
@@ -0,0 +1,17 @@
import { pageTree } from '../source';
import { DocsLayout } from 'fumadocs-ui/layout';
import type { ReactNode } from 'react';
export default function RootDocsLayout({ children }: { children: ReactNode }) {
return (
<DocsLayout
tree={pageTree}
nav={{
title: <span className="text-lg font-bold py-2">Unkey Docs</span>,
}}
sidebar={{ defaultOpenLevel: 0 }}
>
{children}
</DocsLayout>
);
}
+20
View File
@@ -0,0 +1,20 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
background-image: radial-gradient(
800px 500px at 90% 0%,
theme('colors.fd-primary.DEFAULT / 10%'),
transparent
),
radial-gradient(
800px 500px at 50% 0%,
theme('colors.blue.300 / 5%'),
transparent
);
}
.prose {
--tw-prose-body: hsl(var(--muted-foreground));
}
+18
View File
@@ -0,0 +1,18 @@
import { RootProvider } from 'fumadocs-ui/provider';
import './global.css';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
const inter = Inter({
subsets: ['latin'],
});
export default function Layout({ children }: { children: ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>
<RootProvider>{children}</RootProvider>
</body>
</html>
);
}
+38
View File
@@ -0,0 +1,38 @@
import Link from 'next/link';
export default function HomePage() {
return (
<main
style={{
height: '100vh',
display: 'flex',
flexDirection: 'column',
textAlign: 'center',
justifyContent: 'center',
}}
>
<h1
style={{
fontSize: '2rem',
fontWeight: 'bold',
marginBottom: '1rem',
}}
>
Hello World
</h1>
<p>
You can open{' '}
<Link
href="/docs"
style={{
fontWeight: '600',
textDecoration: 'underline',
}}
>
/docs
</Link>{' '}
and see the documentation.
</p>
</main>
);
}
+22
View File
@@ -0,0 +1,22 @@
import { map } from '@/.map';
import { createMDXSource, defaultSchemas } from 'fumadocs-mdx';
import { loader } from 'fumadocs-core/source';
import { attachFile } from 'fumadocs-openapi/source';
import { z } from 'zod';
const frontmatter = defaultSchemas.frontmatter.extend({
method: z.string().optional(),
});
export const { getPage, getPages, pageTree } = loader({
baseUrl: '/docs',
rootDir: 'docs',
source: createMDXSource(map, {
schema: {
frontmatter,
},
}),
pageTree: {
attachFile,
},
});
+17
View File
@@ -0,0 +1,17 @@
---
title: 'Authentication'
description: "How to authenticate your requests to Unkey's API"
---
You'll need to authenticate your requests to access some of the endpoints in the Unkey API. In this guide, we'll look at how authentication works.
## Bearer Token
When requesting resources, you will need your root key — you will find it in the [Dashboard](https://app.unkey.com/settings/root-keys). Here's how to add the root key to the request header using cURL:
```bash
curl https://api.unkey.dev/v1/... \
-H "Authorization: Bearer unkey_xxx"
```
Always keep your root key safe and reset it if you suspect it has been compromised.
+36
View File
@@ -0,0 +1,36 @@
---
title: Overview
description: General information about the API.
---
The Unkey API uses HTTP RPC-style methods and generally follow the schema:
```
https://api.unkey.dev/{version}/{service}.{method}
```
For example `GET https://api.unkey.dev/v1/apis.listKeys` to list all keys for an API.
## HTTP Methods
We strictly use only `GET` and `POST` methods. `PUT` and `DELETE` are not used.
### `GET`
`GET` methods are used for reading data. Filtering, sorting, or pagination is done via query parameters.
```http
curl "https://api.unkey.dev/v1/keys.getKey?keyId=key_123" \
-H "Authorization: Bearer <ROOT_KEY>"
```
### `POST`
`POST` methods are used for creating, updating, and deleting data. Data is passed as `application/json` in the request body.
```http
curl -XPOST "https://api.unkey.dev/v1/keys.createKey" \
-H "Authorization: Bearer <ROOT_KEY>" \
-H "Content-Type: application/json" \
-d '{"apiId": "api_123", "name": "My Key"}'
```
+9
View File
@@ -0,0 +1,9 @@
{
"pages": [
"---API Documentation---",
"index",
"...",
"---References---",
"...(api)"
]
}
+9
View File
@@ -0,0 +1,9 @@
import type { MDXComponents } from 'mdx/types';
import defaultComponents from 'fumadocs-ui/mdx';
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...defaultComponents,
...components,
};
}
+10
View File
@@ -0,0 +1,10 @@
import createMDX from 'fumadocs-mdx/config';
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
};
export default withMDX(config);
+30
View File
@@ -0,0 +1,30 @@
{
"name": "example-openapi",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"build:docs": "node ./scripts/generate-docs.mjs",
"dev": "next dev",
"start": "next start"
},
"dependencies": {
"fumadocs-core": "workspace:*",
"fumadocs-mdx": "workspace:*",
"fumadocs-openapi": "workspace:*",
"fumadocs-ui": "workspace:*",
"next": "^14.2.5",
"postcss": "^8.4.39",
"react": "18.3.1",
"react-dom": "18.3.1",
"tailwindcss": "^3.4.6",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/mdx": "^2.0.13",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"rimraf": "^6.0.1",
"typescript": "^5.5.4"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
@@ -0,0 +1,15 @@
import * as OpenAPI from 'fumadocs-openapi';
import { rimrafSync } from 'rimraf';
rimrafSync('./content/docs/(api)', {
filter(v) {
return !v.endsWith('index.mdx') && !v.endsWith('meta.json');
},
});
void OpenAPI.generateFiles({
input: ['./unkey.json'],
output: './content/docs/(api)',
per: 'operation',
groupBy: 'tag',
});
+19
View File
@@ -0,0 +1,19 @@
// @ts-check
import { createPreset } from 'fumadocs-ui/tailwind-plugin';
/** @type {import('tailwindcss').Config} */
export default {
content: [
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./content/**/*.mdx',
'./mdx-components.tsx',
'./node_modules/fumadocs-ui/dist/**/*.js',
'./node_modules/fumadocs-openapi/dist/**/*.js',
],
presets: [
createPreset({
preset: 'dusk',
}),
],
};
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+5 -7
View File
@@ -61,10 +61,10 @@ export async function generateFiles({
await Promise.all(
resolvedInputs.map(async (path) => {
let filename = parse(path).name;
filename = nameFn?.('file', filename) ?? filename;
if (per === 'file') {
let filename = parse(path).name;
if (nameFn) filename = nameFn('file', filename);
const outPath = join(outputDir, `${filename}.mdx`);
const result = await generate(path, options);
@@ -97,14 +97,12 @@ export async function generateFiles({
outPath = join(
outputDir,
filename,
getFilename(result.method.tags[0]),
`${getFilename(id)}.mdx`,
);
} else if (groupBy === 'route') {
outPath = join(
outputDir,
filename,
result.route.summary
? getFilename(result.route.summary)
: getFilenameFromRoute(result.route.path),
@@ -124,7 +122,7 @@ export async function generateFiles({
console.log(`Generated Meta: ${metaFile}`);
}
} else {
outPath = join(outputDir, filename, `${getFilename(id)}.mdx`);
outPath = join(outputDir, `${getFilename(id)}.mdx`);
}
await write(outPath, result.content);
@@ -140,7 +138,7 @@ export async function generateFiles({
let tagName = result.tag;
tagName = nameFn?.('tag', tagName) ?? getFilename(tagName);
const outPath = join(outputDir, filename, `${tagName}.mdx`);
const outPath = join(outputDir, `${tagName}.mdx`);
await write(outPath, result.content);
console.log(`Generated: ${outPath}`);
}
+50 -11
View File
@@ -292,6 +292,55 @@ importers:
specifier: ^5.5.4
version: 5.5.4
examples/openapi:
dependencies:
fumadocs-core:
specifier: workspace:*
version: link:../../packages/core
fumadocs-mdx:
specifier: workspace:*
version: link:../../packages/mdx
fumadocs-openapi:
specifier: workspace:*
version: link:../../packages/openapi
fumadocs-ui:
specifier: workspace:*
version: link:../../packages/ui
next:
specifier: ^14.2.5
version: 14.2.5(@babel/[email protected])(@opentelemetry/[email protected])([email protected]([email protected]))([email protected])
postcss:
specifier: ^8.4.39
version: 8.4.39
react:
specifier: 18.3.1
version: 18.3.1
react-dom:
specifier: 18.3.1
version: 18.3.1([email protected])
tailwindcss:
specifier: ^3.4.6
version: 3.4.6
zod:
specifier: ^3.23.8
version: 3.23.8
devDependencies:
'@types/mdx':
specifier: ^2.0.13
version: 2.0.13
'@types/react':
specifier: ^18.3.3
version: 18.3.3
'@types/react-dom':
specifier: ^18.3.0
version: 18.3.0
rimraf:
specifier: ^6.0.1
version: 6.0.1
typescript:
specifier: ^5.5.4
version: 5.5.4
examples/remote-mdx:
dependencies:
'@fumadocs/mdx-remote':
@@ -5411,10 +5460,6 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
[email protected]:
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
engines: {node: ^10 || ^12 || >=14}
[email protected]:
resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==}
engines: {node: ^10 || ^12 || >=14}
@@ -9645,7 +9690,7 @@ snapshots:
[email protected]([email protected]):
dependencies:
fast-glob: 3.3.2
postcss: 8.4.38
postcss: 8.4.39
tailwindcss: 3.4.6
[email protected]([email protected])([email protected]):
@@ -11711,12 +11756,6 @@ snapshots:
picocolors: 1.0.1
source-map-js: 1.2.0
[email protected]:
dependencies:
nanoid: 3.3.7
picocolors: 1.0.1
source-map-js: 1.2.0
[email protected]:
dependencies:
nanoid: 3.3.7