mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
OpenAPI: Support meta.json file generation
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'fumadocs-openapi': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Support `meta.json` file generation
|
||||||
Vendored
+3
@@ -11,6 +11,9 @@
|
|||||||
"files.associations": {
|
"files.associations": {
|
||||||
"**/packages/create-app/template/**/*": "plaintext"
|
"**/packages/create-app/template/**/*": "plaintext"
|
||||||
},
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "oxc.oxc-vscode"
|
||||||
|
},
|
||||||
"prettier.enable": false,
|
"prettier.enable": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "oxc.oxc-vscode",
|
"editor.defaultFormatter": "oxc.oxc-vscode",
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import * as path from 'node:path';
|
|||||||
import { generateDocument, type PagesToTextOptions, toText } from './utils/pages/to-text';
|
import { generateDocument, type PagesToTextOptions, toText } from './utils/pages/to-text';
|
||||||
import type { ProcessedDocument } from '@/utils/process-document';
|
import type { ProcessedDocument } from '@/utils/process-document';
|
||||||
import type { OpenAPIServer } from '@/server';
|
import type { OpenAPIServer } from '@/server';
|
||||||
import { createGetUrl, getSlugs } from 'fumadocs-core/source';
|
import { createGetUrl, getSlugs, PathUtils } from 'fumadocs-core/source';
|
||||||
import { createAutoPreset, type SchemaToPagesOptions } from '@/utils/pages/preset-auto';
|
import { createAutoPreset, type SchemaToPagesOptions } from '@/utils/pages/preset-auto';
|
||||||
import { fromSchema, type OutputEntry } from '@/utils/pages/builder';
|
import { fromSchema, type OutputGroup, type OutputEntry } from '@/utils/pages/builder';
|
||||||
|
|
||||||
export interface OutputFile {
|
export interface OutputFile {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -30,6 +30,7 @@ interface IndexConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IndexItem {
|
interface IndexItem {
|
||||||
|
/** output path of index page */
|
||||||
path: string;
|
path: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -37,7 +38,7 @@ interface IndexItem {
|
|||||||
/**
|
/**
|
||||||
* Specify linked pages:
|
* Specify linked pages:
|
||||||
* - items in `inputs` to include all generated pages of a specific schema.
|
* - items in `inputs` to include all generated pages of a specific schema.
|
||||||
* - specific Markdown/MDX files.
|
* - file paths (using forward slash).
|
||||||
*/
|
*/
|
||||||
only?: string[];
|
only?: string[];
|
||||||
}
|
}
|
||||||
@@ -58,12 +59,23 @@ interface GenerateFilesConfig extends PagesToTextOptions {
|
|||||||
*/
|
*/
|
||||||
index?: IndexConfig;
|
index?: IndexConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate `meta.json` files.
|
||||||
|
*
|
||||||
|
* Note: for flexibility, it's recommended to define them on your own.
|
||||||
|
*/
|
||||||
|
meta?: boolean | MetaOptions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can add/change/remove output files before writing to file system
|
* Can add/change/remove output files before writing to file system
|
||||||
**/
|
**/
|
||||||
beforeWrite?: (this: BeforeWriteContext, files: OutputFile[]) => void | Promise<void>;
|
beforeWrite?: (this: BeforeWriteContext, files: OutputFile[]) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MetaOptions {
|
||||||
|
groupStyle?: 'folder' | 'separator';
|
||||||
|
}
|
||||||
|
|
||||||
export type Config = SchemaToPagesOptions & GenerateFilesConfig;
|
export type Config = SchemaToPagesOptions & GenerateFilesConfig;
|
||||||
|
|
||||||
interface BeforeWriteContext {
|
interface BeforeWriteContext {
|
||||||
@@ -106,15 +118,20 @@ export async function generateFilesOnly(
|
|||||||
const schemaFiles = (generated[id] ??= []);
|
const schemaFiles = (generated[id] ??= []);
|
||||||
|
|
||||||
generatedEntries[id] = entries;
|
generatedEntries[id] = entries;
|
||||||
for (const entry of entries) {
|
function scan(entry: OutputEntry) {
|
||||||
const file: OutputFile = {
|
if (entry.type === 'group') {
|
||||||
|
for (const child of entry.entries) scan(child);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaFiles.push({
|
||||||
path: entry.path,
|
path: entry.path,
|
||||||
content: toText(entry, schema, options),
|
content: toText(entry, schema, options),
|
||||||
};
|
});
|
||||||
|
|
||||||
schemaFiles.push(file);
|
|
||||||
files.push(file);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) scan(entry);
|
||||||
|
files.push(...schemaFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
const context: BeforeWriteContext = {
|
const context: BeforeWriteContext = {
|
||||||
@@ -124,21 +141,82 @@ export async function generateFilesOnly(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.index) {
|
if (options.index) {
|
||||||
writeIndexFiles(files, context, options);
|
files.push(...writeIndexFiles(context, options));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.meta) {
|
||||||
|
files.push(...generateMeta(context, options.meta === true ? {} : options.meta));
|
||||||
}
|
}
|
||||||
|
|
||||||
await options.beforeWrite?.call(context, files);
|
await options.beforeWrite?.call(context, files);
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateMeta(context: BeforeWriteContext, options: MetaOptions): OutputFile[] {
|
||||||
|
const files: OutputFile[] = [];
|
||||||
|
const { groupStyle = 'folder' } = options;
|
||||||
|
|
||||||
|
function scan(entries: OutputEntry[], parent?: OutputGroup) {
|
||||||
|
const pages: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const relativePath = PathUtils.slash(
|
||||||
|
parent ? path.relative(parent.path, entry.path) : entry.path,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entry.type === 'group') {
|
||||||
|
scan(entry.entries, entry);
|
||||||
|
|
||||||
|
if (groupStyle === 'folder') {
|
||||||
|
pages.push(relativePath);
|
||||||
|
} else {
|
||||||
|
pages.push(`---${entry.info.title}---`, `...${relativePath}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push(relativePath.slice(0, -path.extname(entry.path).length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length === 0) return;
|
||||||
|
files.push({
|
||||||
|
path: parent ? path.join(parent.path, 'meta.json') : 'meta.json',
|
||||||
|
content: JSON.stringify(
|
||||||
|
{
|
||||||
|
title: parent?.info.title,
|
||||||
|
description: parent?.info.description,
|
||||||
|
pages,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entries of Object.values(context.generatedEntries)) {
|
||||||
|
scan(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
function writeIndexFiles(
|
function writeIndexFiles(
|
||||||
files: OutputFile[],
|
|
||||||
context: BeforeWriteContext,
|
context: BeforeWriteContext,
|
||||||
options: SchemaToPagesOptions & Omit<GenerateFilesConfig, 'output'>,
|
options: SchemaToPagesOptions & Omit<GenerateFilesConfig, 'output'>,
|
||||||
) {
|
): OutputFile[] {
|
||||||
|
const files: OutputFile[] = [];
|
||||||
const { generatedEntries } = context;
|
const { generatedEntries } = context;
|
||||||
|
const pathToEntry = new Map<string, OutputEntry>();
|
||||||
const { items, url } = options.index!;
|
const { items, url } = options.index!;
|
||||||
|
|
||||||
|
function indexEntry(entry: OutputEntry) {
|
||||||
|
pathToEntry.set(PathUtils.slash(entry.path), entry);
|
||||||
|
if (entry.type === 'group') {
|
||||||
|
for (const child of entry.entries) {
|
||||||
|
indexEntry(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let urlFn: (path: string) => string;
|
let urlFn: (path: string) => string;
|
||||||
if (typeof url === 'object') {
|
if (typeof url === 'object') {
|
||||||
const getUrl = createGetUrl(url.baseUrl);
|
const getUrl = createGetUrl(url.baseUrl);
|
||||||
@@ -148,43 +226,37 @@ function writeIndexFiles(
|
|||||||
urlFn = url;
|
urlFn = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findEntryByPath(path: string) {
|
|
||||||
for (const entries of Object.values(generatedEntries)) {
|
|
||||||
const match = entries.find((entry) => entry.path === path);
|
|
||||||
|
|
||||||
if (match) return match;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileContent(index: IndexItem): string {
|
function fileContent(index: IndexItem): string {
|
||||||
const content: string[] = [];
|
const content: string[] = [];
|
||||||
content.push('<Cards>');
|
content.push('<Cards>');
|
||||||
const pathToEntry = new Map<string, OutputEntry>();
|
const outputEntries: OutputEntry[] = [];
|
||||||
const only = index.only ?? Object.keys(context.generated);
|
const only = index.only ?? Object.keys(context.generated);
|
||||||
|
|
||||||
for (const item of only) {
|
for (const item of only) {
|
||||||
if (generatedEntries[item]) {
|
if (generatedEntries[item]) {
|
||||||
for (const entry of generatedEntries[item]) {
|
for (const entry of generatedEntries[item]) {
|
||||||
pathToEntry.set(entry.path, entry);
|
outputEntries.push(entry);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const match = findEntryByPath(item);
|
const match = pathToEntry.get(item);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`${item} does not exist on "input", available: ${Object.keys(generatedEntries).join(', ')}.`,
|
`${item} does not exist on "input", available: ${Object.keys(generatedEntries).join(', ')}.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pathToEntry.set(match.path, match);
|
outputEntries.push(match);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of pathToEntry.values()) {
|
for (const entry of outputEntries) {
|
||||||
const descriptionAttr = file.info.description
|
// cannot link to groups
|
||||||
? `description=${JSON.stringify(file.info.description)} `
|
if (entry.type === 'group') continue;
|
||||||
|
const descriptionAttr = entry.info.description
|
||||||
|
? `description=${JSON.stringify(entry.info.description)} `
|
||||||
: '';
|
: '';
|
||||||
content.push(
|
content.push(
|
||||||
`<Card href="${urlFn(file.path)}" title=${JSON.stringify(file.info.title)} ${descriptionAttr}/>`,
|
`<Card href="${urlFn(entry.path)}" title=${JSON.stringify(entry.info.title)} ${descriptionAttr}/>`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,10 +271,16 @@ function writeIndexFiles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const list of Object.values(context.generatedEntries)) {
|
||||||
|
for (const item of list) indexEntry(item);
|
||||||
|
}
|
||||||
|
|
||||||
for (const item of typeof items === 'function' ? items(context) : items) {
|
for (const item of typeof items === 'function' ? items(context) : items) {
|
||||||
files.push({
|
files.push({
|
||||||
path: path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path,
|
path: path.extname(item.path).length === 0 ? `${item.path}.mdx` : item.path,
|
||||||
content: fileContent(item),
|
content: fileContent(item),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { MethodLabel } from '@/ui/components/method-label';
|
import { MethodLabel } from '@/ui/components/method-label';
|
||||||
import type {
|
import {
|
||||||
LoaderPlugin,
|
PathUtils,
|
||||||
PageData,
|
type LoaderPlugin,
|
||||||
PageTreeTransformer,
|
type MetaData,
|
||||||
Source,
|
type PageData,
|
||||||
VirtualFile,
|
type PageTreeTransformer,
|
||||||
|
type Source,
|
||||||
|
type VirtualFile,
|
||||||
} from 'fumadocs-core/source';
|
} from 'fumadocs-core/source';
|
||||||
import type { OpenAPIServer } from '@/server/create';
|
import type { OpenAPIServer } from '@/server/create';
|
||||||
import type { SchemaToPagesOptions } from '@/utils/pages/preset-auto';
|
import type { SchemaToPagesOptions } from '@/utils/pages/preset-auto';
|
||||||
@@ -12,6 +14,13 @@ import type { ApiPageProps } from '@/ui/api-page';
|
|||||||
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
import type { StructuredData } from 'fumadocs-core/mdx-plugins';
|
||||||
import type { TOCItemType } from 'fumadocs-core/toc';
|
import type { TOCItemType } from 'fumadocs-core/toc';
|
||||||
import type { ProcessedDocument } from '@/utils/process-document';
|
import type { ProcessedDocument } from '@/utils/process-document';
|
||||||
|
import type {
|
||||||
|
OperationOutput,
|
||||||
|
OutputEntry,
|
||||||
|
PageOutput,
|
||||||
|
WebhookOutput,
|
||||||
|
} from '@/utils/pages/builder';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
declare module 'fumadocs-core/source' {
|
declare module 'fumadocs-core/source' {
|
||||||
export interface PageData {
|
export interface PageData {
|
||||||
@@ -76,6 +85,10 @@ interface OpenAPIPageData extends PageData {
|
|||||||
toc: TOCItemType[];
|
toc: TOCItemType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MetaOptions {
|
||||||
|
folderStyle?: 'folder' | 'separator';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate virtual pages for Fumadocs Source API
|
* Generate virtual pages for Fumadocs Source API
|
||||||
*/
|
*/
|
||||||
@@ -83,29 +96,30 @@ export async function openapiSource(
|
|||||||
server: OpenAPIServer,
|
server: OpenAPIServer,
|
||||||
options: SchemaToPagesOptions & {
|
options: SchemaToPagesOptions & {
|
||||||
baseDir?: string;
|
baseDir?: string;
|
||||||
|
/** Generate `meta.json` files */
|
||||||
|
meta?: boolean | MetaOptions;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<
|
): Promise<
|
||||||
Source<{
|
Source<{
|
||||||
metaData: never;
|
metaData: MetaData;
|
||||||
pageData: OpenAPIPageData;
|
pageData: OpenAPIPageData;
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
const { baseDir = '' } = options;
|
const { baseDir = '', meta = false } = options;
|
||||||
const { createAutoPreset } = await import('@/utils/pages/preset-auto');
|
const { createAutoPreset } = await import('@/utils/pages/preset-auto');
|
||||||
const { fromServer } = await import('@/utils/pages/builder');
|
const { fromServer } = await import('@/utils/pages/builder');
|
||||||
const { toBody } = await import('@/utils/pages/to-body');
|
|
||||||
const { toStaticData } = await import('@/utils/pages/to-static-data');
|
const { toStaticData } = await import('@/utils/pages/to-static-data');
|
||||||
const files: VirtualFile<{
|
const files: VirtualFile<{
|
||||||
pageData: OpenAPIPageData;
|
pageData: OpenAPIPageData;
|
||||||
metaData: never;
|
metaData: MetaData;
|
||||||
}>[] = [];
|
}>[] = [];
|
||||||
|
|
||||||
const entries = await fromServer(server, createAutoPreset(options));
|
const entries = await fromServer(server, createAutoPreset(options));
|
||||||
for (const [schemaId, list] of Object.entries(entries)) {
|
for (const [schemaId, list] of Object.entries(entries)) {
|
||||||
const processed = await server.getSchema(schemaId);
|
const processed = await server.getSchema(schemaId);
|
||||||
for (const entry of list) {
|
|
||||||
const props = toBody(entry);
|
function onEntry(entry: PageOutput | OperationOutput | WebhookOutput) {
|
||||||
props.showDescription ??= true;
|
const props = getProps(entry);
|
||||||
|
|
||||||
files.push({
|
files.push({
|
||||||
type: 'page',
|
type: 'page',
|
||||||
@@ -132,6 +146,54 @@ export async function openapiSource(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onEntries(entries: OutputEntry[], parent?: OutputEntry) {
|
||||||
|
if (!meta) {
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.type === 'group') {
|
||||||
|
onEntries(entry.entries, entry);
|
||||||
|
} else {
|
||||||
|
onEntry(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { folderStyle = 'folder' } = meta === true ? {} : meta;
|
||||||
|
const pages: string[] = [];
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const relativePath = PathUtils.slash(
|
||||||
|
parent ? path.relative(parent.path, entry.path) : entry.path,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entry.type === 'group') {
|
||||||
|
onEntries(entry.entries, entry);
|
||||||
|
if (folderStyle === 'folder') {
|
||||||
|
pages.push(relativePath);
|
||||||
|
} else {
|
||||||
|
pages.push(`---${entry.info.title}---`, `...${relativePath}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onEntry(entry);
|
||||||
|
pages.push(relativePath.slice(0, -path.extname(entry.path).length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pages.length === 0) return;
|
||||||
|
files.push({
|
||||||
|
type: 'meta',
|
||||||
|
path: path.join(baseDir, parent?.path ?? '', 'meta.json'),
|
||||||
|
data: {
|
||||||
|
title: parent?.info.title,
|
||||||
|
description: parent?.info.description,
|
||||||
|
pages,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEntries(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -139,6 +201,29 @@ export async function openapiSource(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getProps(entry: PageOutput | OperationOutput | WebhookOutput): ApiPageProps {
|
||||||
|
if (entry.type === 'operation')
|
||||||
|
return {
|
||||||
|
document: entry.schemaId,
|
||||||
|
operations: [entry.item],
|
||||||
|
showDescription: true,
|
||||||
|
};
|
||||||
|
if (entry.type === 'webhook')
|
||||||
|
return {
|
||||||
|
document: entry.schemaId,
|
||||||
|
webhooks: [entry.item],
|
||||||
|
showDescription: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
showTitle: true,
|
||||||
|
showDescription: true,
|
||||||
|
document: entry.schemaId,
|
||||||
|
operations: entry.operations,
|
||||||
|
webhooks: entry.webhooks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated use `openapiPlugin()`
|
* @deprecated use `openapiPlugin()`
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ProcessedDocument } from '@/utils/process-document';
|
import type { ProcessedDocument } from '@/utils/process-document';
|
||||||
import type { OpenAPIServer } from '@/server';
|
import type { OpenAPIServer } from '@/server';
|
||||||
import type { OperationItem, WebhookItem } from '@/ui/api-page';
|
import type { OperationItem, WebhookItem } from '@/ui/api-page';
|
||||||
import type { Document, OperationObject, PathItemObject, TagObject } from '@/types';
|
import type { OperationObject, PathItemObject, TagObject } from '@/types';
|
||||||
import { getTagDisplayName, methodKeys, type NoReference } from '@/utils/schema';
|
import { getTagDisplayName, methodKeys, type NoReference } from '@/utils/schema';
|
||||||
import { idToTitle } from '@/utils/id-to-title';
|
import { idToTitle } from '@/utils/id-to-title';
|
||||||
|
|
||||||
@@ -24,21 +24,22 @@ export interface WebhookOutput extends BaseEntry {
|
|||||||
item: WebhookItem;
|
item: WebhookItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagOutput extends BaseEntry {
|
export interface PageOutput extends BaseEntry {
|
||||||
type: 'tag';
|
type: 'page';
|
||||||
tag: string;
|
|
||||||
rawTag: TagObject;
|
|
||||||
operations: OperationItem[];
|
operations: OperationItem[];
|
||||||
webhooks: WebhookItem[];
|
webhooks: WebhookItem[];
|
||||||
|
/** tag info if the page is generated from a tag. */
|
||||||
|
tag?: TagObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OutputGroup extends BaseEntry {
|
export interface OutputGroup extends BaseEntry {
|
||||||
type: 'group';
|
type: 'group';
|
||||||
operations: OperationItem[];
|
entries: OutputEntry[];
|
||||||
webhooks: WebhookItem[];
|
/** tag info if the group is generated from a tag. */
|
||||||
|
tag?: TagObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OutputEntry = TagOutput | OperationOutput | WebhookOutput | OutputGroup;
|
export type OutputEntry = PageOutput | OperationOutput | WebhookOutput | OutputGroup;
|
||||||
|
|
||||||
export interface PagesBuilderConfig {
|
export interface PagesBuilderConfig {
|
||||||
toPages: (builder: PagesBuilder) => void;
|
toPages: (builder: PagesBuilder) => void;
|
||||||
@@ -52,8 +53,6 @@ export interface PagesBuilder {
|
|||||||
document: ProcessedDocument;
|
document: ProcessedDocument;
|
||||||
/**
|
/**
|
||||||
* add output entry.
|
* add output entry.
|
||||||
*
|
|
||||||
* When the `path` property is unspecified, it will generate one.
|
|
||||||
*/
|
*/
|
||||||
create: (entry: OutputEntry) => void;
|
create: (entry: OutputEntry) => void;
|
||||||
|
|
||||||
@@ -132,7 +131,39 @@ export function fromSchema(
|
|||||||
create(entry) {
|
create(entry) {
|
||||||
files.push(entry);
|
files.push(entry);
|
||||||
},
|
},
|
||||||
extract: () => extractInfo(dereferenced),
|
extract() {
|
||||||
|
const result: ExtractedInfo = { webhooks: [], operations: [] };
|
||||||
|
|
||||||
|
for (const [path, pathItem] of Object.entries(dereferenced.paths ?? {})) {
|
||||||
|
if (!pathItem) continue;
|
||||||
|
|
||||||
|
for (const methodKey of methodKeys) {
|
||||||
|
if (!pathItem[methodKey]) continue;
|
||||||
|
|
||||||
|
result.operations.push({
|
||||||
|
method: methodKey,
|
||||||
|
path,
|
||||||
|
tags: pathItem[methodKey]?.tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, pathItem] of Object.entries(dereferenced.webhooks ?? {})) {
|
||||||
|
if (!pathItem) continue;
|
||||||
|
|
||||||
|
for (const methodKey of methodKeys) {
|
||||||
|
if (!pathItem[methodKey]) continue;
|
||||||
|
|
||||||
|
result.webhooks.push({
|
||||||
|
method: methodKey,
|
||||||
|
name,
|
||||||
|
tags: pathItem[methodKey]?.tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
routePathToFilePath(path) {
|
routePathToFilePath(path) {
|
||||||
return path
|
return path
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -195,37 +226,3 @@ export function fromSchema(
|
|||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractInfo(document: NoReference<Document>): ExtractedInfo {
|
|
||||||
const result: ExtractedInfo = { webhooks: [], operations: [] };
|
|
||||||
|
|
||||||
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
|
|
||||||
if (!pathItem) continue;
|
|
||||||
|
|
||||||
for (const methodKey of methodKeys) {
|
|
||||||
if (!pathItem[methodKey]) continue;
|
|
||||||
|
|
||||||
result.operations.push({
|
|
||||||
method: methodKey,
|
|
||||||
path,
|
|
||||||
tags: pathItem[methodKey]?.tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [name, pathItem] of Object.entries(document.webhooks ?? {})) {
|
|
||||||
if (!pathItem) continue;
|
|
||||||
|
|
||||||
for (const methodKey of methodKeys) {
|
|
||||||
if (!pathItem[methodKey]) continue;
|
|
||||||
|
|
||||||
result.webhooks.push({
|
|
||||||
method: methodKey,
|
|
||||||
name,
|
|
||||||
tags: pathItem[methodKey]?.tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ import type {
|
|||||||
OperationOutput,
|
OperationOutput,
|
||||||
OutputEntry,
|
OutputEntry,
|
||||||
OutputGroup,
|
OutputGroup,
|
||||||
|
PageOutput,
|
||||||
PagesBuilder,
|
PagesBuilder,
|
||||||
PagesBuilderConfig,
|
PagesBuilderConfig,
|
||||||
TagOutput,
|
|
||||||
WebhookOutput,
|
WebhookOutput,
|
||||||
} from '@/utils/pages/builder';
|
} from '@/utils/pages/builder';
|
||||||
import { isUrl } from '@/utils/url';
|
import { isUrl } from '@/utils/url';
|
||||||
|
import type { DistributiveOmit } from '@/types';
|
||||||
|
|
||||||
interface OperationConfig extends BaseConfig {
|
interface OperationConfig extends BaseConfig {
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +27,11 @@ interface OperationConfig extends BaseConfig {
|
|||||||
*
|
*
|
||||||
* @defaultValue 'none'
|
* @defaultValue 'none'
|
||||||
*/
|
*/
|
||||||
groupBy?: 'tag' | 'route' | 'none' | ((entry: OperationOutput | WebhookOutput) => string);
|
groupBy?:
|
||||||
|
| 'tag'
|
||||||
|
| 'route'
|
||||||
|
| 'none'
|
||||||
|
| ((entry: DistributiveOmit<OperationOutput | WebhookOutput, 'path'>) => string);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify name for output file
|
* Specify name for output file
|
||||||
@@ -43,7 +48,7 @@ interface TagConfig extends BaseConfig {
|
|||||||
/**
|
/**
|
||||||
* Specify name for output file
|
* Specify name for output file
|
||||||
*/
|
*/
|
||||||
name?: NameFn<TagOutput> | NameFnOptions;
|
name?: NameFn<PageOutput> | NameFnOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SchemaConfig extends BaseConfig {
|
interface SchemaConfig extends BaseConfig {
|
||||||
@@ -55,7 +60,7 @@ interface SchemaConfig extends BaseConfig {
|
|||||||
/**
|
/**
|
||||||
* Specify name for output file
|
* Specify name for output file
|
||||||
*/
|
*/
|
||||||
name?: NameFn<OutputGroup> | NameFnOptions;
|
name?: NameFn<PageOutput> | NameFnOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SchemaToPagesOptions =
|
export type SchemaToPagesOptions =
|
||||||
@@ -66,9 +71,14 @@ export type SchemaToPagesOptions =
|
|||||||
per: 'custom';
|
per: 'custom';
|
||||||
} & PagesBuilderConfig);
|
} & PagesBuilderConfig);
|
||||||
|
|
||||||
type NameFn<Entry> = (
|
type NameFn<
|
||||||
|
Entry extends OperationOutput | WebhookOutput | PageOutput =
|
||||||
|
| OperationOutput
|
||||||
|
| WebhookOutput
|
||||||
|
| PageOutput,
|
||||||
|
> = (
|
||||||
this: PagesBuilder,
|
this: PagesBuilder,
|
||||||
output: Entry,
|
output: DistributiveOmit<Entry, 'path'>,
|
||||||
document: ProcessedDocument['dereferenced'],
|
document: ProcessedDocument['dereferenced'],
|
||||||
) => string;
|
) => string;
|
||||||
|
|
||||||
@@ -100,19 +110,16 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
|||||||
return s.replace(/\s+/g, '-').toLowerCase();
|
return s.replace(/\s+/g, '-').toLowerCase();
|
||||||
},
|
},
|
||||||
} = options;
|
} = options;
|
||||||
let nameFn: NameFn<OutputEntry>;
|
let nameFn: NameFn;
|
||||||
|
|
||||||
if (typeof options.name === 'function') {
|
if (typeof options.name === 'function') {
|
||||||
nameFn = options.name as NameFn<OutputEntry>;
|
nameFn = options.name as NameFn;
|
||||||
} else {
|
} else {
|
||||||
const { algorithm = 'v2' } = options.name ?? {};
|
const { algorithm = 'v2' } = options.name ?? {};
|
||||||
|
|
||||||
nameFn = function (result, document) {
|
nameFn = function (result, document) {
|
||||||
if (result.type === 'tag') {
|
if (result.type === 'page') {
|
||||||
return slugify(result.tag);
|
if (result.tag) return slugify(result.tag.name!);
|
||||||
}
|
|
||||||
|
|
||||||
if (result.type === 'group') {
|
|
||||||
const schemaId = result.schemaId;
|
const schemaId = result.schemaId;
|
||||||
|
|
||||||
return isUrl(schemaId) ? 'index' : path.basename(schemaId, path.extname(schemaId));
|
return isUrl(schemaId) ? 'index' : path.basename(schemaId, path.extname(schemaId));
|
||||||
@@ -141,44 +148,114 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupOutput(builder: PagesBuilder, entry: OperationOutput | WebhookOutput): string[] {
|
function group(
|
||||||
|
builder: PagesBuilder,
|
||||||
|
entries: DistributiveOmit<OperationOutput | WebhookOutput, 'path'>[],
|
||||||
|
): OutputEntry[] {
|
||||||
|
const groups = new Map<string, OutputGroup>();
|
||||||
|
const rest: OutputEntry[] = [];
|
||||||
const { dereferenced } = builder.document;
|
const { dereferenced } = builder.document;
|
||||||
const { groupBy = 'none' } = options as OperationConfig;
|
const { groupBy = 'none' } = options as OperationConfig;
|
||||||
|
|
||||||
if (groupBy === 'route') {
|
for (const entry of entries) {
|
||||||
return [
|
switch (groupBy) {
|
||||||
path.join(
|
case 'route': {
|
||||||
builder.routePathToFilePath(
|
const groupName = builder.routePathToFilePath(
|
||||||
entry.type === 'operation' ? entry.item.path : entry.item.name,
|
entry.type === 'operation' ? entry.item.path : entry.item.name,
|
||||||
),
|
);
|
||||||
`${entry.item.method.toLowerCase()}.mdx`,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = nameFn.call(builder, entry, dereferenced);
|
let group = groups.get(groupName);
|
||||||
if (groupBy === 'tag') {
|
if (!group) {
|
||||||
let tags =
|
group = {
|
||||||
entry.type === 'operation'
|
type: 'group',
|
||||||
? dereferenced.paths![entry.item.path]![entry.item.method]!.tags
|
info: { title: groupName },
|
||||||
: dereferenced.webhooks![entry.item.name][entry.item.method]!.tags;
|
entries: [],
|
||||||
|
schemaId: builder.id,
|
||||||
|
path: groupName,
|
||||||
|
};
|
||||||
|
groups.set(groupName, group);
|
||||||
|
}
|
||||||
|
|
||||||
if (!tags || tags.length === 0) {
|
group.entries.push({
|
||||||
console.warn(
|
...entry,
|
||||||
'When `groupBy` is set to `tag`, make sure a `tags` is defined for every operation schema.',
|
path: path.join(groupName, `${entry.item.method.toLowerCase()}.mdx`),
|
||||||
);
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'tag': {
|
||||||
|
let tags =
|
||||||
|
entry.type === 'operation'
|
||||||
|
? dereferenced.paths![entry.item.path]![entry.item.method]!.tags
|
||||||
|
: dereferenced.webhooks![entry.item.name][entry.item.method]!.tags;
|
||||||
|
|
||||||
tags = ['unknown'];
|
if (!tags || tags.length === 0) {
|
||||||
|
console.warn(
|
||||||
|
'When `groupBy` is set to `tag`, make sure a `tags` is defined for every operation schema.',
|
||||||
|
);
|
||||||
|
|
||||||
|
tags = ['unknown'];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
const groupName = slugify(tag);
|
||||||
|
const { displayName, info } = builder.fromTagName(tag)!;
|
||||||
|
let group = groups.get(groupName);
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
type: 'group',
|
||||||
|
info: { title: displayName, description: info.description },
|
||||||
|
tag: info,
|
||||||
|
entries: [],
|
||||||
|
schemaId: builder.id,
|
||||||
|
path: groupName,
|
||||||
|
};
|
||||||
|
groups.set(groupName, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
group.entries.push({
|
||||||
|
...entry,
|
||||||
|
path: path.join(groupName, `${nameFn.call(builder, entry, dereferenced)}.mdx`),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const fileName = `${nameFn.call(builder, entry, dereferenced)}.mdx`;
|
||||||
|
|
||||||
|
if (typeof groupBy === 'function') {
|
||||||
|
const groupDisplayName = groupBy(entry);
|
||||||
|
const groupName = slugify(groupDisplayName);
|
||||||
|
|
||||||
|
let group = groups.get(groupName);
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
type: 'group',
|
||||||
|
info: { title: groupDisplayName },
|
||||||
|
entries: [],
|
||||||
|
schemaId: builder.id,
|
||||||
|
path: groupName,
|
||||||
|
};
|
||||||
|
groups.set(groupName, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
group.entries.push({
|
||||||
|
...entry,
|
||||||
|
path: path.join(groupName, fileName),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
rest.push({
|
||||||
|
...entry,
|
||||||
|
path: fileName,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tags.map((tag) => path.join(slugify(tag), `${file}.mdx`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof groupBy === 'function') {
|
rest.push(...groups.values());
|
||||||
return [path.join(slugify(groupBy(entry)), `${file}.mdx`)];
|
return rest;
|
||||||
}
|
|
||||||
|
|
||||||
return [`${file}.mdx`];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -187,8 +264,8 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
|||||||
const items = builder.extract();
|
const items = builder.extract();
|
||||||
|
|
||||||
if (options.per === 'file') {
|
if (options.per === 'file') {
|
||||||
const entry: OutputGroup = {
|
const entry: PageOutput = {
|
||||||
type: 'group',
|
type: 'page',
|
||||||
schemaId: builder.id,
|
schemaId: builder.id,
|
||||||
path: '',
|
path: '',
|
||||||
info: {
|
info: {
|
||||||
@@ -197,17 +274,18 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
|||||||
},
|
},
|
||||||
...items,
|
...items,
|
||||||
};
|
};
|
||||||
entry.path = nameFn.call(builder, entry, dereferenced) + '.mdx';
|
entry.path = `${nameFn.call(builder, entry, dereferenced)}.mdx`;
|
||||||
builder.create(entry);
|
builder.create(entry);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.per === 'tag') {
|
if (options.per === 'tag') {
|
||||||
const tags = dereferenced.tags ?? [];
|
const tags = dereferenced.tags ?? [];
|
||||||
|
|
||||||
for (const tag of tags) {
|
for (const tag of tags) {
|
||||||
const { displayName } = builder.fromTag(tag);
|
const { displayName } = builder.fromTag(tag);
|
||||||
const entry: TagOutput = {
|
const entry: PageOutput = {
|
||||||
type: 'tag',
|
type: 'page',
|
||||||
path: '',
|
path: '',
|
||||||
schemaId: builder.id,
|
schemaId: builder.id,
|
||||||
info: {
|
info: {
|
||||||
@@ -216,40 +294,35 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
|||||||
},
|
},
|
||||||
webhooks: items.webhooks.filter((webhook) => webhook.tags?.includes(tag.name!)),
|
webhooks: items.webhooks.filter((webhook) => webhook.tags?.includes(tag.name!)),
|
||||||
operations: items.operations.filter((op) => op.tags?.includes(tag.name!)),
|
operations: items.operations.filter((op) => op.tags?.includes(tag.name!)),
|
||||||
tag: tag.name!,
|
tag,
|
||||||
rawTag: tag,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
entry.path = nameFn.call(builder, entry, dereferenced) + '.mdx';
|
entry.path = `${nameFn.call(builder, entry, dereferenced)}.mdx`;
|
||||||
builder.create(entry);
|
builder.create(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const entries: DistributiveOmit<OperationOutput | WebhookOutput, 'path'>[] = [];
|
||||||
for (const op of items.operations) {
|
for (const op of items.operations) {
|
||||||
const { pathItem, operation, displayName } = builder.fromExtractedOperation(op)!;
|
const { pathItem, operation, displayName } = builder.fromExtractedOperation(op)!;
|
||||||
|
|
||||||
const entry: OperationOutput = {
|
entries.push({
|
||||||
type: 'operation',
|
type: 'operation',
|
||||||
schemaId: builder.id,
|
schemaId: builder.id,
|
||||||
item: op,
|
item: op,
|
||||||
path: '',
|
|
||||||
info: {
|
info: {
|
||||||
title: displayName,
|
title: displayName,
|
||||||
description: operation.description ?? pathItem.description,
|
description: operation.description ?? pathItem.description,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
for (const outputPath of groupOutput(builder, entry)) {
|
|
||||||
builder.create({ ...entry, path: outputPath });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const webhook of items.webhooks) {
|
for (const webhook of items.webhooks) {
|
||||||
const { pathItem, operation, displayName } = builder.fromExtractedWebhook(webhook)!;
|
const { pathItem, operation, displayName } = builder.fromExtractedWebhook(webhook)!;
|
||||||
|
|
||||||
const entry: WebhookOutput = {
|
entries.push({
|
||||||
type: 'webhook',
|
type: 'webhook',
|
||||||
schemaId: builder.id,
|
schemaId: builder.id,
|
||||||
info: {
|
info: {
|
||||||
@@ -257,12 +330,11 @@ export function createAutoPreset(options: SchemaToPagesOptions): PagesBuilderCon
|
|||||||
description: operation.description ?? pathItem.description,
|
description: operation.description ?? pathItem.description,
|
||||||
},
|
},
|
||||||
item: webhook,
|
item: webhook,
|
||||||
path: '',
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
for (const outputPath of groupOutput(builder, entry)) {
|
for (const entry of group(builder, entries)) {
|
||||||
builder.create({ ...entry, path: outputPath });
|
builder.create(entry);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import type { ApiPageProps } from '@/ui/api-page';
|
|
||||||
import type { OutputEntry } from '@/utils/pages/builder';
|
|
||||||
|
|
||||||
export function toBody(entry: OutputEntry): ApiPageProps {
|
|
||||||
if (entry.type === 'operation')
|
|
||||||
return {
|
|
||||||
document: entry.schemaId,
|
|
||||||
operations: [entry.item],
|
|
||||||
};
|
|
||||||
if (entry.type === 'webhook')
|
|
||||||
return {
|
|
||||||
document: entry.schemaId,
|
|
||||||
webhooks: [entry.item],
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
showTitle: true,
|
|
||||||
showDescription: true,
|
|
||||||
document: entry.schemaId,
|
|
||||||
operations: entry.operations,
|
|
||||||
webhooks: entry.webhooks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@ import type { ProcessedDocument } from '@/utils/process-document';
|
|||||||
import type { TagObject } from '@/types';
|
import type { TagObject } from '@/types';
|
||||||
import { dump } from 'js-yaml';
|
import { dump } from 'js-yaml';
|
||||||
import { removeUndefined } from '@/utils/remove-undefined';
|
import { removeUndefined } from '@/utils/remove-undefined';
|
||||||
import type { OutputEntry } from '@/utils/pages/builder';
|
import type { OperationOutput, PageOutput, WebhookOutput } from '@/utils/pages/builder';
|
||||||
import type { InternalOpenAPIMeta } from '@/server/source-api';
|
import type { InternalOpenAPIMeta } from '@/server/source-api';
|
||||||
import { toStaticData } from '@/utils/pages/to-static-data';
|
import { toStaticData } from '@/utils/pages/to-static-data';
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export interface PagesToTextOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function toText(
|
export function toText(
|
||||||
entry: OutputEntry,
|
entry: PageOutput | OperationOutput | WebhookOutput,
|
||||||
processed: ProcessedDocument,
|
processed: ProcessedDocument,
|
||||||
options: PagesToTextOptions = {},
|
options: PagesToTextOptions = {},
|
||||||
) {
|
) {
|
||||||
@@ -68,7 +68,7 @@ export function toText(
|
|||||||
type: 'operation',
|
type: 'operation',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
case 'group':
|
case 'page':
|
||||||
return generatePage(
|
return generatePage(
|
||||||
entry.schemaId,
|
entry.schemaId,
|
||||||
processed,
|
processed,
|
||||||
@@ -81,27 +81,14 @@ export function toText(
|
|||||||
...options,
|
...options,
|
||||||
...entry.info,
|
...entry.info,
|
||||||
},
|
},
|
||||||
{
|
entry.tag
|
||||||
type: 'file',
|
? {
|
||||||
},
|
type: 'tag',
|
||||||
);
|
tag: entry.tag,
|
||||||
case 'tag':
|
}
|
||||||
return generatePage(
|
: {
|
||||||
entry.schemaId,
|
type: 'file',
|
||||||
processed,
|
},
|
||||||
{
|
|
||||||
operations: entry.operations,
|
|
||||||
webhooks: entry.webhooks,
|
|
||||||
showTitle: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...options,
|
|
||||||
...entry.info,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'tag',
|
|
||||||
tag: entry.rawTag,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
return generatePage(
|
return generatePage(
|
||||||
|
|||||||
@@ -129,6 +129,35 @@ describe('Generate documents', () => {
|
|||||||
|
|
||||||
await expect(stringifyOutput(out)).toMatchFileSnapshot('./out/products-with-index.md');
|
await expect(stringifyOutput(out)).toMatchFileSnapshot('./out/products-with-index.md');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Generate Files - with meta', async () => {
|
||||||
|
const out = await generateFilesOnly({
|
||||||
|
input: createOpenAPI({
|
||||||
|
input: () => ({
|
||||||
|
products: path.join(cwd, './fixtures/products.yaml'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
per: 'operation',
|
||||||
|
meta: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(stringifyOutput(out)).toMatchFileSnapshot('./out/products-with-meta.md');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Generate Files - with meta + groupBy', async () => {
|
||||||
|
const out = await generateFilesOnly({
|
||||||
|
input: createOpenAPI({
|
||||||
|
input: () => ({
|
||||||
|
products: path.join(cwd, './fixtures/products.yaml'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
per: 'operation',
|
||||||
|
groupBy: 'tag',
|
||||||
|
meta: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(stringifyOutput(out)).toMatchFileSnapshot('./out/products-with-meta+groupby.md');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function stringifyOutput(output: OutputFile[]) {
|
function stringifyOutput(output: OutputFile[]) {
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
```mdx title="inventory/getInventoryForProduct.mdx"
|
||||||
|
---
|
||||||
|
title: Get product inventory
|
||||||
|
description: Retrieve inventory details for a specific product.
|
||||||
|
full: true
|
||||||
|
_openapi:
|
||||||
|
method: GET
|
||||||
|
toc: []
|
||||||
|
structuredData:
|
||||||
|
headings: []
|
||||||
|
contents:
|
||||||
|
- content: Retrieve inventory details for a specific product.
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||||
|
|
||||||
|
<APIPage document={"products"} operations={[{"path":"/inventory/{productId}","method":"get"}]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
```json title="inventory/meta.json"
|
||||||
|
{
|
||||||
|
"title": "Inventory",
|
||||||
|
"description": "Operations related to inventory.",
|
||||||
|
"pages": [
|
||||||
|
"getInventoryForProduct"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json title="meta.json"
|
||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"products",
|
||||||
|
"inventory"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```mdx title="products/getInventoryForProduct.mdx"
|
||||||
|
---
|
||||||
|
title: Get product inventory
|
||||||
|
description: Retrieve inventory details for a specific product.
|
||||||
|
full: true
|
||||||
|
_openapi:
|
||||||
|
method: GET
|
||||||
|
toc: []
|
||||||
|
structuredData:
|
||||||
|
headings: []
|
||||||
|
contents:
|
||||||
|
- content: Retrieve inventory details for a specific product.
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||||
|
|
||||||
|
<APIPage document={"products"} operations={[{"path":"/inventory/{productId}","method":"get"}]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
```mdx title="products/getProductDetails.mdx"
|
||||||
|
---
|
||||||
|
title: Get product details
|
||||||
|
description: Retrieve details of a product by its ID.
|
||||||
|
full: true
|
||||||
|
_openapi:
|
||||||
|
method: GET
|
||||||
|
toc: []
|
||||||
|
structuredData:
|
||||||
|
headings: []
|
||||||
|
contents:
|
||||||
|
- content: Retrieve details of a product by its ID.
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||||
|
|
||||||
|
<APIPage document={"products"} operations={[{"path":"/products/{productId}","method":"get"}]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
```json title="products/meta.json"
|
||||||
|
{
|
||||||
|
"title": "Products",
|
||||||
|
"description": "Operations related to products.",
|
||||||
|
"pages": [
|
||||||
|
"getProductDetails",
|
||||||
|
"getInventoryForProduct"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
```mdx title="getInventoryForProduct.mdx"
|
||||||
|
---
|
||||||
|
title: Get product inventory
|
||||||
|
description: Retrieve inventory details for a specific product.
|
||||||
|
full: true
|
||||||
|
_openapi:
|
||||||
|
method: GET
|
||||||
|
toc: []
|
||||||
|
structuredData:
|
||||||
|
headings: []
|
||||||
|
contents:
|
||||||
|
- content: Retrieve inventory details for a specific product.
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||||
|
|
||||||
|
<APIPage document={"products"} operations={[{"path":"/inventory/{productId}","method":"get"}]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
```mdx title="getProductDetails.mdx"
|
||||||
|
---
|
||||||
|
title: Get product details
|
||||||
|
description: Retrieve details of a product by its ID.
|
||||||
|
full: true
|
||||||
|
_openapi:
|
||||||
|
method: GET
|
||||||
|
toc: []
|
||||||
|
structuredData:
|
||||||
|
headings: []
|
||||||
|
contents:
|
||||||
|
- content: Retrieve details of a product by its ID.
|
||||||
|
---
|
||||||
|
|
||||||
|
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||||
|
|
||||||
|
<APIPage document={"products"} operations={[{"path":"/products/{productId}","method":"get"}]} />
|
||||||
|
```
|
||||||
|
|
||||||
|
```json title="meta.json"
|
||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"getProductDetails",
|
||||||
|
"getInventoryForProduct"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user