mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 06:31:49 +00:00
OpenAPI: Generate document field on output MDX files
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@fumadocs/content-collections': patch
|
||||
'fumadocs-mdx': patch
|
||||
---
|
||||
|
||||
Improve Fumadocs OpenAPI support
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'fumadocs-openapi': minor
|
||||
---
|
||||
|
||||
Generate `document` field on output MDX files
|
||||
@@ -46,9 +46,7 @@ export const blog = loader({
|
||||
}),
|
||||
});
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
documentOrPath: './museum.yaml',
|
||||
});
|
||||
export const openapi = createOpenAPI({});
|
||||
|
||||
export type Page = InferPageType<typeof utils>;
|
||||
export type Meta = InferMetaType<typeof utils>;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { createMDXSource, defaultSchemas } from 'fumadocs-mdx';
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { attachFile, createOpenAPI } from 'fumadocs-openapi/server';
|
||||
import { z } from 'zod';
|
||||
import * as path from 'node:path';
|
||||
|
||||
const frontmatter = defaultSchemas.frontmatter.extend({
|
||||
method: z.string().optional(),
|
||||
@@ -22,6 +21,4 @@ export const { getPage, getPages, pageTree } = loader({
|
||||
},
|
||||
});
|
||||
|
||||
export const openapi = createOpenAPI({
|
||||
documentOrPath: path.resolve('./unkey.json'),
|
||||
});
|
||||
export const openapi = createOpenAPI({});
|
||||
|
||||
@@ -133,6 +133,8 @@ export function createDocSchema(z: typeof Zod) {
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
full: z.boolean().optional(),
|
||||
// Fumadocs OpenAPI generated
|
||||
_openapi: z.object({}).passthrough().optional(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ const frontmatterSchema = z.object({
|
||||
description: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
full: z.boolean().optional(),
|
||||
|
||||
// Fumadocs OpenAPI generated
|
||||
_openapi: z.object({}).passthrough().optional(),
|
||||
});
|
||||
|
||||
export const defaultSchemas = {
|
||||
|
||||
@@ -43,31 +43,43 @@ export interface Config extends GenerateOptions {
|
||||
* @defaultValue 'none'
|
||||
*/
|
||||
groupBy?: 'tag' | 'route' | 'none';
|
||||
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export async function generateFiles({
|
||||
input,
|
||||
output,
|
||||
name: nameFn,
|
||||
per = 'file',
|
||||
cwd = process.cwd(),
|
||||
groupBy = 'none',
|
||||
...options
|
||||
}: Config): Promise<void> {
|
||||
export async function generateFiles(options: Config): Promise<void> {
|
||||
const {
|
||||
input,
|
||||
output,
|
||||
name: nameFn,
|
||||
per = 'file',
|
||||
groupBy = 'none',
|
||||
cwd = process.cwd(),
|
||||
} = options;
|
||||
const outputDir = join(cwd, output);
|
||||
const resolvedInputs = await fg.glob(input, { absolute: true, cwd });
|
||||
const urlInputs: string[] = [];
|
||||
const fileInputs: string[] = [];
|
||||
|
||||
for (const v of typeof input === 'string' ? [input] : input) {
|
||||
if (isUrl(v)) {
|
||||
urlInputs.push(v);
|
||||
} else {
|
||||
fileInputs.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
const resolvedInputs = [
|
||||
...(await fg.glob(fileInputs, { cwd, absolute: false })),
|
||||
...urlInputs,
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
resolvedInputs.map(async (path) => {
|
||||
resolvedInputs.map(async (pathOrUrl) => {
|
||||
if (per === 'file') {
|
||||
let filename = parse(path).name;
|
||||
let filename = isUrl(pathOrUrl) ? 'index' : parse(pathOrUrl).name;
|
||||
if (nameFn) filename = nameFn('file', filename);
|
||||
|
||||
const outPath = join(outputDir, `${filename}.mdx`);
|
||||
|
||||
const result = await generateAll(path, options);
|
||||
const result = await generateAll(pathOrUrl, options);
|
||||
await write(outPath, result);
|
||||
console.log(`Generated: ${outPath}`);
|
||||
return;
|
||||
@@ -75,7 +87,7 @@ export async function generateFiles({
|
||||
|
||||
if (per === 'operation') {
|
||||
const metaFiles = new Set<string>();
|
||||
const results = await generateOperations(path, options);
|
||||
const results = await generateOperations(pathOrUrl, options);
|
||||
|
||||
await Promise.all(
|
||||
results.map(async (result) => {
|
||||
@@ -132,7 +144,7 @@ export async function generateFiles({
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await generateTags(path, options);
|
||||
const results = await generateTags(pathOrUrl, options);
|
||||
|
||||
for (const result of results) {
|
||||
let tagName = result.tag;
|
||||
@@ -146,6 +158,10 @@ export async function generateFiles({
|
||||
);
|
||||
}
|
||||
|
||||
function isUrl(input: string): boolean {
|
||||
return input.startsWith('https://') || input.startsWith('http://');
|
||||
}
|
||||
|
||||
function getFilenameFromRoute(path: string): string {
|
||||
return (
|
||||
path
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { resolve } from 'node:path';
|
||||
import Parser from '@apidevtools/json-schema-ref-parser';
|
||||
import { type OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import { type TableOfContents } from 'fumadocs-core/server';
|
||||
import { type StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import Slugger from 'github-slugger';
|
||||
import { buildRoutes } from '@/build-routes';
|
||||
import { generateDocument } from '@/utils/generate-document';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
import { type ApiPageProps } from '@/server/api-page';
|
||||
import { type Operation } from '@/server/api-page';
|
||||
import type { MethodInformation, RouteInformation } from './types';
|
||||
|
||||
export type DocumentContext =
|
||||
@@ -52,6 +50,8 @@ export interface GenerateOptions {
|
||||
description: string | undefined,
|
||||
context: DocumentContext,
|
||||
) => Record<string, unknown>;
|
||||
|
||||
cwd?: string;
|
||||
}
|
||||
|
||||
export interface GenerateTagOutput {
|
||||
@@ -66,13 +66,27 @@ export interface GenerateOperationOutput {
|
||||
route: RouteInformation;
|
||||
}
|
||||
|
||||
async function dereference(
|
||||
pathOrDocument: string | OpenAPI.Document,
|
||||
options: GenerateOptions,
|
||||
): Promise<OpenAPI.Document> {
|
||||
return await Parser.dereference<OpenAPI.Document>(
|
||||
// resolve paths
|
||||
typeof pathOrDocument === 'string' &&
|
||||
!pathOrDocument.startsWith('http://') &&
|
||||
!pathOrDocument.startsWith('https://')
|
||||
? resolve(options.cwd ?? process.cwd(), pathOrDocument)
|
||||
: pathOrDocument,
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateAll(
|
||||
pathOrDocument: string | OpenAPI.Document,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<string> {
|
||||
const document = await Parser.dereference<OpenAPI.Document>(pathOrDocument);
|
||||
const document = await dereference(pathOrDocument, options);
|
||||
const routes = buildRoutes(document).get('all') ?? [];
|
||||
const operations: { path: string; method: OpenAPI.HttpMethods }[] = [];
|
||||
const operations: Operation[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
for (const method of route.methods) {
|
||||
@@ -83,25 +97,29 @@ export async function generateAll(
|
||||
}
|
||||
}
|
||||
|
||||
return generateDocument(
|
||||
pageContent(document, { operations, hasHead: true }),
|
||||
options,
|
||||
{
|
||||
title: document.info.title,
|
||||
description: document.info.description,
|
||||
context: {
|
||||
type: 'file',
|
||||
routes,
|
||||
},
|
||||
return generateDocument({
|
||||
...options,
|
||||
dereferenced: document,
|
||||
title: document.info.title,
|
||||
description: document.info.description,
|
||||
page: {
|
||||
operations,
|
||||
hasHead: true,
|
||||
document: pathOrDocument,
|
||||
},
|
||||
);
|
||||
|
||||
context: {
|
||||
type: 'file',
|
||||
routes,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function generateOperations(
|
||||
pathOrDocument: string | OpenAPI.Document,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<GenerateOperationOutput[]> {
|
||||
const document = await Parser.dereference<OpenAPI.Document>(pathOrDocument);
|
||||
const document = await dereference(pathOrDocument, options);
|
||||
const routes = buildRoutes(document).get('all') ?? [];
|
||||
|
||||
return routes.flatMap<GenerateOperationOutput>((route) => {
|
||||
@@ -109,8 +127,9 @@ export async function generateOperations(
|
||||
if (!method.operationId)
|
||||
throw new Error('Operation ID is required for generating docs.');
|
||||
|
||||
const content = generateDocument(
|
||||
pageContent(document, {
|
||||
const content = generateDocument({
|
||||
...options,
|
||||
page: {
|
||||
operations: [
|
||||
{
|
||||
path: route.path,
|
||||
@@ -118,18 +137,17 @@ export async function generateOperations(
|
||||
},
|
||||
],
|
||||
hasHead: false,
|
||||
}),
|
||||
options,
|
||||
{
|
||||
title: method.summary ?? idToTitle(method.operationId),
|
||||
description: method.description,
|
||||
context: {
|
||||
type: 'operation',
|
||||
endpoint: method,
|
||||
route,
|
||||
},
|
||||
document: pathOrDocument,
|
||||
},
|
||||
);
|
||||
dereferenced: document,
|
||||
title: method.summary ?? idToTitle(method.operationId),
|
||||
description: method.description,
|
||||
context: {
|
||||
type: 'operation',
|
||||
endpoint: method,
|
||||
route,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
content,
|
||||
@@ -144,14 +162,14 @@ export async function generateTags(
|
||||
pathOrDocument: string | OpenAPI.Document,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<GenerateTagOutput[]> {
|
||||
const document = await Parser.dereference<OpenAPI.Document>(pathOrDocument);
|
||||
const document = await dereference(pathOrDocument, options);
|
||||
const tags = Array.from(buildRoutes(document).entries());
|
||||
|
||||
return tags
|
||||
.filter(([tag]) => tag !== 'all')
|
||||
.map(([tag, routes]) => {
|
||||
const info = document.tags?.find((t) => t.name === tag);
|
||||
const operations: { path: string; method: OpenAPI.HttpMethods }[] = [];
|
||||
const operations: Operation[] = [];
|
||||
|
||||
for (const route of routes) {
|
||||
for (const method of route.methods) {
|
||||
@@ -164,76 +182,22 @@ export async function generateTags(
|
||||
|
||||
return {
|
||||
tag,
|
||||
content: generateDocument(
|
||||
pageContent(document, { operations, hasHead: true }),
|
||||
options,
|
||||
{
|
||||
title: idToTitle(tag),
|
||||
description: info?.description,
|
||||
context: {
|
||||
type: 'tag',
|
||||
tag: info,
|
||||
routes,
|
||||
},
|
||||
content: generateDocument({
|
||||
...options,
|
||||
page: {
|
||||
document: pathOrDocument,
|
||||
operations,
|
||||
hasHead: true,
|
||||
},
|
||||
),
|
||||
dereferenced: document,
|
||||
title: idToTitle(tag),
|
||||
description: info?.description,
|
||||
context: {
|
||||
type: 'tag',
|
||||
tag: info,
|
||||
routes,
|
||||
},
|
||||
}),
|
||||
} satisfies GenerateTagOutput;
|
||||
});
|
||||
}
|
||||
|
||||
function pageContent(
|
||||
doc: OpenAPI.Document,
|
||||
props: Omit<ApiPageProps, 'document'>,
|
||||
): string {
|
||||
const slugger = new Slugger();
|
||||
const toc: TableOfContents = [];
|
||||
const structuredData: StructuredData = { headings: [], contents: [] };
|
||||
|
||||
for (const item of props.operations) {
|
||||
const operation = doc.paths[item.path]?.[item.method];
|
||||
if (!operation) continue;
|
||||
|
||||
if (props.hasHead && operation.operationId) {
|
||||
const title =
|
||||
operation.summary ??
|
||||
(operation.operationId ? idToTitle(operation.operationId) : item.path);
|
||||
const id = slugger.slug(title);
|
||||
|
||||
toc.push({
|
||||
depth: 2,
|
||||
title,
|
||||
url: `#${id}`,
|
||||
});
|
||||
structuredData.headings.push({
|
||||
content: title,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.description)
|
||||
structuredData.contents.push({
|
||||
content: operation.description,
|
||||
heading: structuredData.headings.at(-1)?.id,
|
||||
});
|
||||
}
|
||||
|
||||
// modify toc and structured data if possible
|
||||
// it may not be compatible with other content sources except Fumadocs MDX
|
||||
// TODO: Maybe add to frontmatter and let developers to handle them?
|
||||
return `<APIPage operations={${JSON.stringify(props.operations)}} hasHead={${JSON.stringify(props.hasHead)}} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
// toc might be immutable
|
||||
while (toc.length > 0) toc.pop()
|
||||
toc.push(...${JSON.stringify(toc)})
|
||||
}
|
||||
|
||||
if (typeof structuredData !== 'undefined') {
|
||||
structuredData.headings = ${JSON.stringify(structuredData.headings)}
|
||||
structuredData.contents = ${JSON.stringify(structuredData.contents)}
|
||||
}
|
||||
}
|
||||
|
||||
{startup()}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { type OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import Slugger from 'github-slugger';
|
||||
import Parser from '@apidevtools/json-schema-ref-parser';
|
||||
import { Operation } from '@/render/operation';
|
||||
import type { RenderContext } from '@/types';
|
||||
import { createMethod } from '@/schema/method';
|
||||
@@ -11,18 +12,35 @@ export interface ApiPageProps
|
||||
RenderContext,
|
||||
'generateCodeSamples' | 'generateTypeScriptSchema'
|
||||
> {
|
||||
document: OpenAPI.Document;
|
||||
document: string | OpenAPI.Document;
|
||||
|
||||
/**
|
||||
* An array of operation
|
||||
* An array of operations
|
||||
*/
|
||||
operations: { path: string; method: OpenAPI.HttpMethods }[];
|
||||
operations: Operation[];
|
||||
hasHead: boolean;
|
||||
renderer?: Partial<Renderer>;
|
||||
}
|
||||
|
||||
export function APIPage(props: ApiPageProps): ReactElement {
|
||||
const { operations, document, hasHead = true } = props;
|
||||
const cache = new Map<string, OpenAPI.Document>();
|
||||
|
||||
export interface Operation {
|
||||
path: string;
|
||||
method: OpenAPI.HttpMethods;
|
||||
}
|
||||
|
||||
export async function APIPage(props: ApiPageProps): Promise<ReactElement> {
|
||||
const { operations, hasHead = true } = props;
|
||||
let document: OpenAPI.Document;
|
||||
|
||||
if (typeof props.document === 'string') {
|
||||
const cached = cache.get(props.document);
|
||||
document =
|
||||
cached ?? (await Parser.dereference<OpenAPI.Document>(props.document));
|
||||
cache.set(props.document, document);
|
||||
} else {
|
||||
document = await Parser.dereference<OpenAPI.Document>(props.document);
|
||||
}
|
||||
|
||||
const ctx = getContext(document, props);
|
||||
return (
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import type { OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import Parser from '@apidevtools/json-schema-ref-parser';
|
||||
import { type FC } from 'react';
|
||||
import { APIPage, type ApiPageProps } from '@/server/api-page';
|
||||
|
||||
export interface OpenAPIOptions
|
||||
extends Omit<Partial<ApiPageProps>, 'document'> {
|
||||
documentOrPath: string | OpenAPI.Document;
|
||||
/**
|
||||
* @deprecated Pass document to `APIPage` instead
|
||||
*/
|
||||
documentOrPath?: string | OpenAPI.Document;
|
||||
}
|
||||
|
||||
export interface OpenAPIServer {
|
||||
APIPage: FC<Omit<ApiPageProps, 'document'>>;
|
||||
APIPage: FC<ApiPageProps>;
|
||||
}
|
||||
|
||||
export function createOpenAPI(options: OpenAPIOptions): OpenAPIServer {
|
||||
const document = Parser.dereference(options.documentOrPath);
|
||||
|
||||
return {
|
||||
APIPage: async (props) => {
|
||||
return (
|
||||
<APIPage document={(await document) as OpenAPI.Document} {...props} />
|
||||
);
|
||||
APIPage(props) {
|
||||
return <APIPage {...props} />;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,9 +10,20 @@ import { getBadgeColor } from '@/ui/components/variants';
|
||||
export const attachFile: BuildPageTreeOptions['attachFile'] = (node, file) => {
|
||||
if (!file) return node;
|
||||
const data = file.data.data as object;
|
||||
let method: string | undefined;
|
||||
|
||||
if ('method' in data && typeof data.method === 'string') {
|
||||
const color = getBadgeColor(data.method);
|
||||
if ('_openapi' in data && typeof data._openapi === 'object') {
|
||||
const meta = data._openapi as {
|
||||
method?: string;
|
||||
};
|
||||
|
||||
method = meta.method;
|
||||
} else if ('method' in data && typeof data.method === 'string') {
|
||||
method = data.method;
|
||||
}
|
||||
|
||||
if (method) {
|
||||
const color = getBadgeColor(method);
|
||||
|
||||
node.name = (
|
||||
<>
|
||||
@@ -20,7 +31,7 @@ export const attachFile: BuildPageTreeOptions['attachFile'] = (node, file) => {
|
||||
<span
|
||||
className={badgeVariants({ className: 'ms-auto text-nowrap', color })}
|
||||
>
|
||||
{data.method}
|
||||
{method}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,31 +1,55 @@
|
||||
import { dump } from 'js-yaml';
|
||||
import { type OpenAPIV3 as OpenAPI } from 'openapi-types';
|
||||
import Slugger from 'github-slugger';
|
||||
import { type TableOfContents } from 'fumadocs-core/server';
|
||||
import { type StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||
import { type ApiPageProps } from '@/server/api-page';
|
||||
import type { DocumentContext, GenerateOptions } from '@/generate';
|
||||
import { idToTitle } from '@/utils/id-to-title';
|
||||
|
||||
interface StaticData {
|
||||
toc: TableOfContents;
|
||||
structuredData: StructuredData;
|
||||
}
|
||||
|
||||
export function generateDocument(
|
||||
content: string,
|
||||
options: GenerateOptions,
|
||||
frontmatter: {
|
||||
options: GenerateOptions & {
|
||||
dereferenced: OpenAPI.Document;
|
||||
page: ApiPageProps;
|
||||
|
||||
title: string;
|
||||
description?: string;
|
||||
context: DocumentContext;
|
||||
},
|
||||
): string {
|
||||
const { frontmatter } = options;
|
||||
const out: string[] = [];
|
||||
const extend = frontmatter?.(
|
||||
options.title,
|
||||
options.description,
|
||||
options.context,
|
||||
);
|
||||
|
||||
let meta: object | undefined;
|
||||
if (options.context.type === 'operation') {
|
||||
meta = {
|
||||
method: options.context.endpoint.method,
|
||||
route: options.context.route.path,
|
||||
};
|
||||
}
|
||||
|
||||
const data = generateStaticData(options.dereferenced, options.page);
|
||||
|
||||
const banner = dump({
|
||||
title: frontmatter.title,
|
||||
description: frontmatter.description,
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
full: true,
|
||||
...(frontmatter.context.type === 'operation'
|
||||
? {
|
||||
method: frontmatter.context.endpoint.method,
|
||||
route: frontmatter.context.route.path,
|
||||
}
|
||||
: undefined),
|
||||
...options.frontmatter?.(
|
||||
frontmatter.title,
|
||||
frontmatter.description,
|
||||
frontmatter.context,
|
||||
),
|
||||
...extend,
|
||||
_openapi: {
|
||||
...meta,
|
||||
...data,
|
||||
...(extend?._openapi as object | undefined),
|
||||
},
|
||||
}).trim();
|
||||
if (banner.length > 0) out.push(`---\n${banner}\n---`);
|
||||
|
||||
@@ -40,7 +64,68 @@ export function generateDocument(
|
||||
out.push(imports);
|
||||
}
|
||||
|
||||
out.push(content);
|
||||
out.push(pageContent(data, options.page));
|
||||
|
||||
return out.join('\n\n');
|
||||
}
|
||||
|
||||
function generateStaticData(
|
||||
dereferenced: OpenAPI.Document,
|
||||
props: ApiPageProps,
|
||||
): StaticData {
|
||||
const slugger = new Slugger();
|
||||
const toc: TableOfContents = [];
|
||||
const structuredData: StructuredData = { headings: [], contents: [] };
|
||||
|
||||
for (const item of props.operations) {
|
||||
const operation = dereferenced.paths[item.path]?.[item.method];
|
||||
if (!operation) continue;
|
||||
|
||||
if (props.hasHead && operation.operationId) {
|
||||
const title =
|
||||
operation.summary ??
|
||||
(operation.operationId ? idToTitle(operation.operationId) : item.path);
|
||||
const id = slugger.slug(title);
|
||||
|
||||
toc.push({
|
||||
depth: 2,
|
||||
title,
|
||||
url: `#${id}`,
|
||||
});
|
||||
structuredData.headings.push({
|
||||
content: title,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
if (operation.description)
|
||||
structuredData.contents.push({
|
||||
content: operation.description,
|
||||
heading: structuredData.headings.at(-1)?.id,
|
||||
});
|
||||
}
|
||||
|
||||
return { toc, structuredData };
|
||||
}
|
||||
|
||||
function pageContent(data: StaticData, props: ApiPageProps): string {
|
||||
// modify toc and structured data if possible
|
||||
// it may not be compatible with other content sources except Fumadocs MDX
|
||||
// TODO: Maybe add to frontmatter and let developers to handle them?
|
||||
return `<APIPage document={${JSON.stringify(props.document)}} operations={${JSON.stringify(props.operations)}} hasHead={${JSON.stringify(props.hasHead)}} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
// toc might be immutable
|
||||
while (toc.length > 0) toc.pop()
|
||||
toc.push(...${JSON.stringify(data.toc)})
|
||||
}
|
||||
|
||||
if (typeof structuredData !== 'undefined') {
|
||||
structuredData.headings = ${JSON.stringify(data.structuredData.headings)}
|
||||
structuredData.contents = ${JSON.stringify(data.structuredData.contents)}
|
||||
}
|
||||
}
|
||||
|
||||
{startup()}`;
|
||||
}
|
||||
|
||||
@@ -13,22 +13,24 @@ describe('Utilities', () => {
|
||||
});
|
||||
|
||||
describe('Generate documents', () => {
|
||||
const cwd = fileURLToPath(new URL('./', import.meta.url));
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('Pet Store', async () => {
|
||||
const result = await generateAll(
|
||||
fileURLToPath(new URL('./fixtures/petstore.yaml', import.meta.url)),
|
||||
);
|
||||
const result = await generateAll('./fixtures/petstore.yaml', {
|
||||
cwd,
|
||||
});
|
||||
|
||||
await expect(result).toMatchFileSnapshot('./out/petstore.mdx');
|
||||
});
|
||||
|
||||
test('Museum', async () => {
|
||||
const tags = await generateTags(
|
||||
fileURLToPath(new URL('./fixtures/museum.yaml', import.meta.url)),
|
||||
);
|
||||
const tags = await generateTags('./fixtures/museum.yaml', {
|
||||
cwd,
|
||||
});
|
||||
|
||||
for (const tag of tags) {
|
||||
await expect(tag.content).toMatchFileSnapshot(
|
||||
@@ -38,9 +40,7 @@ describe('Generate documents', () => {
|
||||
});
|
||||
|
||||
test('Unkey', async () => {
|
||||
const tags = await generateTags(
|
||||
fileURLToPath(new URL('./fixtures/unkey.json', import.meta.url)),
|
||||
);
|
||||
const tags = await generateTags('./fixtures/unkey.json', { cwd });
|
||||
|
||||
for (const tag of tags) {
|
||||
await expect(tag.content).toMatchFileSnapshot(
|
||||
@@ -50,7 +50,6 @@ describe('Generate documents', () => {
|
||||
});
|
||||
|
||||
test('Generate Files', async () => {
|
||||
const cwd = fileURLToPath(new URL('./', import.meta.url));
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
return {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- mock
|
||||
|
||||
@@ -2,9 +2,51 @@
|
||||
title: Events
|
||||
description: Special events hosted by the Museum
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: List special events
|
||||
url: '#list-special-events'
|
||||
- depth: 2
|
||||
title: Create special events
|
||||
url: '#create-special-events'
|
||||
- depth: 2
|
||||
title: Get special event
|
||||
url: '#get-special-event'
|
||||
- depth: 2
|
||||
title: Update special event
|
||||
url: '#update-special-event'
|
||||
- depth: 2
|
||||
title: Delete special event
|
||||
url: '#delete-special-event'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List special events
|
||||
id: list-special-events
|
||||
- content: Create special events
|
||||
id: create-special-events
|
||||
- content: Get special event
|
||||
id: get-special-event
|
||||
- content: Update special event
|
||||
id: update-special-event
|
||||
- content: Delete special event
|
||||
id: delete-special-event
|
||||
contents:
|
||||
- content: Return a list of upcoming special events at the museum.
|
||||
heading: list-special-events
|
||||
- content: Creates a new special event for the museum.
|
||||
heading: create-special-events
|
||||
- content: Get details about a special event.
|
||||
heading: get-special-event
|
||||
- content: Update the details of a special event
|
||||
heading: update-special-event
|
||||
- content: >-
|
||||
Delete a special event from the collection. Allows museum to cancel
|
||||
planned events.
|
||||
heading: delete-special-event
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"get","path":"/special-events"},{"method":"post","path":"/special-events"},{"method":"get","path":"/special-events/{eventId}"},{"method":"patch","path":"/special-events/{eventId}"},{"method":"delete","path":"/special-events/{eventId}"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/museum.yaml"} operations={[{"method":"get","path":"/special-events"},{"method":"post","path":"/special-events"},{"method":"get","path":"/special-events/{eventId}"},{"method":"patch","path":"/special-events/{eventId}"},{"method":"delete","path":"/special-events/{eventId}"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
@@ -2,9 +2,21 @@
|
||||
title: Operations
|
||||
description: Operational information about the museum.
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Get museum hours
|
||||
url: '#get-museum-hours'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Get museum hours
|
||||
id: get-museum-hours
|
||||
contents:
|
||||
- content: Get upcoming museum operating hours
|
||||
heading: get-museum-hours
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"get","path":"/museum-hours"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/museum.yaml"} operations={[{"method":"get","path":"/museum-hours"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
@@ -2,9 +2,30 @@
|
||||
title: Tickets
|
||||
description: Museum tickets for general entrance or special events.
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Buy museum tickets
|
||||
url: '#buy-museum-tickets'
|
||||
- depth: 2
|
||||
title: Get ticket QR code
|
||||
url: '#get-ticket-qr-code'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Buy museum tickets
|
||||
id: buy-museum-tickets
|
||||
- content: Get ticket QR code
|
||||
id: get-ticket-qr-code
|
||||
contents:
|
||||
- content: Purchase museum tickets for general entry or special events.
|
||||
heading: buy-museum-tickets
|
||||
- content: >-
|
||||
Return an image of your ticket with scannable QR code. Used for event
|
||||
entry.
|
||||
heading: get-ticket-qr-code
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"post","path":"/tickets"},{"method":"get","path":"/tickets/{ticketId}/qr"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/museum.yaml"} operations={[{"method":"post","path":"/tickets"},{"method":"get","path":"/tickets/{ticketId}/qr"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
---
|
||||
title: Swagger Petstore
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: List all pets
|
||||
url: '#list-all-pets'
|
||||
- depth: 2
|
||||
title: Create a pet
|
||||
url: '#create-a-pet'
|
||||
- depth: 2
|
||||
title: Info for a specific pet
|
||||
url: '#info-for-a-specific-pet'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List all pets
|
||||
id: list-all-pets
|
||||
- content: Create a pet
|
||||
id: create-a-pet
|
||||
- content: Info for a specific pet
|
||||
id: info-for-a-specific-pet
|
||||
contents: []
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"get","path":"/pets"},{"method":"post","path":"/pets"},{"method":"get","path":"/pets/{petId}"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/petstore.yaml"} operations={[{"method":"get","path":"/pets"},{"method":"post","path":"/pets"},{"method":"get","path":"/pets/{petId}"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
@@ -1,9 +1,34 @@
|
||||
---
|
||||
title: Apis
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Get Api
|
||||
url: '#get-api'
|
||||
- depth: 2
|
||||
title: Create Api
|
||||
url: '#create-api'
|
||||
- depth: 2
|
||||
title: List Keys
|
||||
url: '#list-keys'
|
||||
- depth: 2
|
||||
title: Delete Api
|
||||
url: '#delete-api'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Get Api
|
||||
id: get-api
|
||||
- content: Create Api
|
||||
id: create-api
|
||||
- content: List Keys
|
||||
id: list-keys
|
||||
- content: Delete Api
|
||||
id: delete-api
|
||||
contents: []
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"get","path":"/v1/apis.getApi"},{"method":"post","path":"/v1/apis.createApi"},{"method":"get","path":"/v1/apis.listKeys"},{"method":"post","path":"/v1/apis.deleteApi"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/unkey.json"} operations={[{"method":"get","path":"/v1/apis.getApi"},{"method":"post","path":"/v1/apis.createApi"},{"method":"get","path":"/v1/apis.listKeys"},{"method":"post","path":"/v1/apis.deleteApi"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
---
|
||||
title: Keys
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Get Key
|
||||
url: '#get-key'
|
||||
- depth: 2
|
||||
title: Delete Key
|
||||
url: '#delete-key'
|
||||
- depth: 2
|
||||
title: Create Key
|
||||
url: '#create-key'
|
||||
- depth: 2
|
||||
title: Verify Key
|
||||
url: '#verify-key'
|
||||
- depth: 2
|
||||
title: Update Key
|
||||
url: '#update-key'
|
||||
- depth: 2
|
||||
title: Update Remaining
|
||||
url: '#update-remaining'
|
||||
- depth: 2
|
||||
title: Get Verifications
|
||||
url: '#get-verifications'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Get Key
|
||||
id: get-key
|
||||
- content: Delete Key
|
||||
id: delete-key
|
||||
- content: Create Key
|
||||
id: create-key
|
||||
- content: Verify Key
|
||||
id: verify-key
|
||||
- content: Update Key
|
||||
id: update-key
|
||||
- content: Update Remaining
|
||||
id: update-remaining
|
||||
- content: Get Verifications
|
||||
id: get-verifications
|
||||
contents: []
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"get","path":"/v1/keys.getKey"},{"method":"post","path":"/v1/keys.deleteKey"},{"method":"post","path":"/v1/keys.createKey"},{"method":"post","path":"/v1/keys.verifyKey"},{"method":"post","path":"/v1/keys.updateKey"},{"method":"post","path":"/v1/keys.updateRemaining"},{"method":"get","path":"/v1/keys.getVerifications"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/unkey.json"} operations={[{"method":"get","path":"/v1/keys.getKey"},{"method":"post","path":"/v1/keys.deleteKey"},{"method":"post","path":"/v1/keys.createKey"},{"method":"post","path":"/v1/keys.verifyKey"},{"method":"post","path":"/v1/keys.updateKey"},{"method":"post","path":"/v1/keys.updateRemaining"},{"method":"get","path":"/v1/keys.getVerifications"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
---
|
||||
title: Liveness
|
||||
full: true
|
||||
_openapi:
|
||||
toc:
|
||||
- depth: 2
|
||||
title: Check Liveness
|
||||
url: '#check-liveness'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: Check Liveness
|
||||
id: check-liveness
|
||||
contents: []
|
||||
---
|
||||
|
||||
<APIPage operations={[{"method":"get","path":"/v1/liveness"}]} hasHead={true} />
|
||||
<APIPage document={"./fixtures/unkey.json"} operations={[{"method":"get","path":"/v1/liveness"}]} hasHead={true} />
|
||||
|
||||
export function startup() {
|
||||
if (typeof toc !== 'undefined') {
|
||||
|
||||
Reference in New Issue
Block a user