@raystack/chronicle 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +4 -1
- package/package.json +2 -1
- package/src/components/api/api-code-snippet.module.css +23 -0
- package/src/components/api/api-code-snippet.tsx +64 -0
- package/src/components/api/api-field-list.module.css +76 -0
- package/src/components/api/api-field-list.tsx +91 -0
- package/src/components/api/api-overview.module.css +65 -0
- package/src/components/api/api-overview.tsx +216 -0
- package/src/components/api/api-response-panel.module.css +62 -0
- package/src/components/api/api-response-panel.tsx +54 -0
- package/src/components/api/index.ts +5 -6
- package/src/components/api/json-editor.tsx +8 -8
- package/src/components/api/method-badge.tsx +2 -2
- package/src/components/api/playground-dialog.module.css +342 -0
- package/src/components/api/playground-dialog.tsx +583 -0
- package/src/lib/api-routes.ts +37 -8
- package/src/lib/openapi.ts +26 -0
- package/src/lib/schema.ts +45 -3
- package/src/lib/source.ts +57 -23
- package/src/lib/use-api-operation.ts +15 -0
- package/src/pages/ApiLayout.module.css +1 -0
- package/src/pages/ApiPage.tsx +7 -38
- package/src/pages/DocsPage.tsx +40 -1
- package/src/server/api/apis-proxy.ts +8 -1
- package/src/server/entry-server.tsx +2 -2
- package/src/server/routes/[...slug].md.ts +1 -0
- package/src/server/routes/apis/[...slug].md.ts +181 -0
- package/src/server/vite-config.ts +2 -0
- package/src/themes/default/Layout.module.css +53 -0
- package/src/themes/default/Layout.tsx +162 -11
- package/src/themes/paper/Page.module.css +7 -2
- package/src/themes/paper/Page.tsx +8 -6
- package/src/themes/paper/Skeleton.tsx +9 -0
- package/src/types/config.ts +1 -0
- package/src/components/api/code-snippets.module.css +0 -7
- package/src/components/api/code-snippets.tsx +0 -76
- package/src/components/api/endpoint-page.module.css +0 -58
- package/src/components/api/endpoint-page.tsx +0 -283
- package/src/components/api/field-row.module.css +0 -126
- package/src/components/api/field-row.tsx +0 -204
- package/src/components/api/field-section.module.css +0 -24
- package/src/components/api/field-section.tsx +0 -100
- package/src/components/api/key-value-editor.module.css +0 -13
- package/src/components/api/key-value-editor.tsx +0 -62
- package/src/components/api/response-panel.module.css +0 -8
- package/src/components/api/response-panel.tsx +0 -44
package/src/lib/openapi.ts
CHANGED
|
@@ -120,12 +120,38 @@ function convertV2toV3(doc: OpenAPIV2.Document): OpenAPIV3.Document {
|
|
|
120
120
|
v3Paths[pathStr] = v3PathItem
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
const securitySchemes = convertV2SecurityDefs(resolved.securityDefinitions as Record<string, OpenAPIV2.SecuritySchemeObject> | undefined)
|
|
124
|
+
|
|
123
125
|
return {
|
|
124
126
|
openapi: '3.0.0',
|
|
125
127
|
info: resolved.info as unknown as OpenAPIV3.InfoObject,
|
|
126
128
|
paths: v3Paths,
|
|
127
129
|
tags: (resolved.tags ?? []) as unknown as OpenAPIV3.TagObject[],
|
|
130
|
+
...(resolved.externalDocs ? { externalDocs: resolved.externalDocs as unknown as OpenAPIV3.ExternalDocumentationObject } : {}),
|
|
131
|
+
...(Object.keys(securitySchemes).length > 0 ? { components: { securitySchemes } } : {}),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function convertV2SecurityDefs(defs: Record<string, OpenAPIV2.SecuritySchemeObject> | undefined): Record<string, OpenAPIV3.SecuritySchemeObject> {
|
|
136
|
+
if (!defs) return {}
|
|
137
|
+
const result: Record<string, OpenAPIV3.SecuritySchemeObject> = {}
|
|
138
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
139
|
+
if (def.type === 'apiKey') {
|
|
140
|
+
result[name] = { type: 'apiKey', name: (def as JsonObject).name as string, in: def.in as string } as OpenAPIV3.ApiKeySecurityScheme
|
|
141
|
+
} else if (def.type === 'basic') {
|
|
142
|
+
result[name] = { type: 'http', scheme: 'basic' } as OpenAPIV3.HttpSecurityScheme
|
|
143
|
+
} else if (def.type === 'oauth2') {
|
|
144
|
+
const v2 = def as unknown as { flow?: string; authorizationUrl?: string; tokenUrl?: string; scopes?: Record<string, string> }
|
|
145
|
+
const flow = { authorizationUrl: v2.authorizationUrl ?? '', tokenUrl: v2.tokenUrl ?? '', scopes: v2.scopes ?? {} }
|
|
146
|
+
const flows: OpenAPIV3.OAuth2SecurityScheme['flows'] = {}
|
|
147
|
+
if (v2.flow === 'implicit') flows.implicit = { authorizationUrl: flow.authorizationUrl, scopes: flow.scopes }
|
|
148
|
+
else if (v2.flow === 'password') flows.password = { tokenUrl: flow.tokenUrl, scopes: flow.scopes }
|
|
149
|
+
else if (v2.flow === 'application') flows.clientCredentials = { tokenUrl: flow.tokenUrl, scopes: flow.scopes }
|
|
150
|
+
else if (v2.flow === 'accessCode') flows.authorizationCode = { authorizationUrl: flow.authorizationUrl, tokenUrl: flow.tokenUrl, scopes: flow.scopes }
|
|
151
|
+
result[name] = { type: 'oauth2', flows } as OpenAPIV3.OAuth2SecurityScheme
|
|
152
|
+
}
|
|
128
153
|
}
|
|
154
|
+
return result
|
|
129
155
|
}
|
|
130
156
|
|
|
131
157
|
function convertV2Operation(op: OpenAPIV2.OperationObject): OpenAPIV3.OperationObject {
|
package/src/lib/schema.ts
CHANGED
|
@@ -1,19 +1,56 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
2
|
|
|
3
|
+
const schemaFieldKinds = {
|
|
4
|
+
string: 'string', integer: 'integer', number: 'number',
|
|
5
|
+
boolean: 'boolean', array: 'array', object: 'object',
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
export type SchemaFieldKind = keyof typeof schemaFieldKinds
|
|
9
|
+
|
|
10
|
+
export function toKind(type: unknown): SchemaFieldKind {
|
|
11
|
+
if (typeof type === 'string' && type in schemaFieldKinds) return type as SchemaFieldKind
|
|
12
|
+
return 'object'
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export interface SchemaField {
|
|
4
16
|
name: string
|
|
5
17
|
type: string
|
|
18
|
+
kind: SchemaFieldKind
|
|
6
19
|
required: boolean
|
|
7
20
|
description?: string
|
|
8
21
|
default?: unknown
|
|
22
|
+
example?: unknown
|
|
9
23
|
enum?: unknown[]
|
|
10
24
|
children?: SchemaField[]
|
|
11
25
|
}
|
|
12
26
|
|
|
27
|
+
function mergeAllOf(schema: OpenAPIV3.SchemaObject): OpenAPIV3.SchemaObject {
|
|
28
|
+
const composed = schema.allOf ?? schema.oneOf ?? schema.anyOf
|
|
29
|
+
if (!composed) return schema
|
|
30
|
+
const merged: OpenAPIV3.SchemaObject = { ...schema }
|
|
31
|
+
delete merged.allOf
|
|
32
|
+
delete merged.oneOf
|
|
33
|
+
delete merged.anyOf
|
|
34
|
+
for (const sub of composed as OpenAPIV3.SchemaObject[]) {
|
|
35
|
+
if (sub.type) merged.type = sub.type
|
|
36
|
+
if (sub.properties) {
|
|
37
|
+
merged.properties = { ...(merged.properties ?? {}), ...sub.properties }
|
|
38
|
+
}
|
|
39
|
+
if (sub.required) {
|
|
40
|
+
merged.required = [...(merged.required ?? []), ...sub.required]
|
|
41
|
+
}
|
|
42
|
+
if (sub.description && !merged.description) merged.description = sub.description
|
|
43
|
+
}
|
|
44
|
+
return merged
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
export function flattenSchema(
|
|
14
48
|
schema: OpenAPIV3.SchemaObject,
|
|
15
49
|
requiredFields: string[] = [],
|
|
16
50
|
): SchemaField[] {
|
|
51
|
+
const resolved = mergeAllOf(schema)
|
|
52
|
+
if (resolved !== schema) return flattenSchema(resolved, requiredFields)
|
|
53
|
+
|
|
17
54
|
if (schema.type === 'array' && schema.items) {
|
|
18
55
|
const items = schema.items as OpenAPIV3.SchemaObject
|
|
19
56
|
const itemType = inferType(items)
|
|
@@ -26,6 +63,7 @@ export function flattenSchema(
|
|
|
26
63
|
return [{
|
|
27
64
|
name: 'items',
|
|
28
65
|
type: `${itemType}[]`,
|
|
66
|
+
kind: 'array' as SchemaFieldKind,
|
|
29
67
|
required: true,
|
|
30
68
|
description: items.description,
|
|
31
69
|
children: children?.length ? children : undefined,
|
|
@@ -36,7 +74,8 @@ export function flattenSchema(
|
|
|
36
74
|
const properties = (schema.properties ?? {}) as Record<string, OpenAPIV3.SchemaObject>
|
|
37
75
|
const required = schema.required ?? requiredFields
|
|
38
76
|
|
|
39
|
-
return Object.entries(properties).map(([name,
|
|
77
|
+
return Object.entries(properties).map(([name, rawProp]) => {
|
|
78
|
+
const prop = mergeAllOf(rawProp)
|
|
40
79
|
const fieldType = inferType(prop)
|
|
41
80
|
const children =
|
|
42
81
|
fieldType === 'object' || prop.properties
|
|
@@ -48,9 +87,11 @@ export function flattenSchema(
|
|
|
48
87
|
return {
|
|
49
88
|
name,
|
|
50
89
|
type: fieldType,
|
|
90
|
+
kind: toKind(prop.type),
|
|
51
91
|
required: required.includes(name),
|
|
52
|
-
description: prop.description,
|
|
92
|
+
description: rawProp.description ?? prop.description,
|
|
53
93
|
default: prop.default,
|
|
94
|
+
example: prop.example,
|
|
54
95
|
enum: prop.enum,
|
|
55
96
|
children: children?.length ? children : undefined,
|
|
56
97
|
}
|
|
@@ -87,7 +128,8 @@ export function generateExampleJson(schema: OpenAPIV3.SchemaObject): unknown {
|
|
|
87
128
|
return defaults[schema.type as string] ?? null
|
|
88
129
|
}
|
|
89
130
|
|
|
90
|
-
function inferType(
|
|
131
|
+
function inferType(rawSchema: OpenAPIV3.SchemaObject): string {
|
|
132
|
+
const schema = mergeAllOf(rawSchema)
|
|
91
133
|
if (schema.type === 'array') {
|
|
92
134
|
const items = schema.items as OpenAPIV3.SchemaObject | undefined
|
|
93
135
|
const itemType = items ? inferType(items) : 'unknown'
|
package/src/lib/source.ts
CHANGED
|
@@ -97,6 +97,8 @@ function buildSyntheticMeta(): {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
let cachedSource: ReturnType<typeof loader> | null = null;
|
|
100
|
+
let cachedTree: Root | null = null;
|
|
101
|
+
let cachedNavMap: Map<string, PageNav> | null = null;
|
|
100
102
|
|
|
101
103
|
async function getSource() {
|
|
102
104
|
if (cachedSource) return cachedSource;
|
|
@@ -112,42 +114,65 @@ export { getSource as source };
|
|
|
112
114
|
|
|
113
115
|
export function invalidate() {
|
|
114
116
|
cachedSource = null;
|
|
117
|
+
cachedTree = null;
|
|
118
|
+
cachedNavMap = null;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
|
-
function getOrder(node: Node,
|
|
118
|
-
if (node.type === 'page') return
|
|
119
|
-
if (node.type === 'folder'
|
|
121
|
+
function getOrder(node: Node, pageOrderMap: Map<string, number>, folderOrderMap: Map<string, number>): number | undefined {
|
|
122
|
+
if (node.type === 'page') return pageOrderMap.get(node.url);
|
|
123
|
+
if (node.type === 'folder') {
|
|
124
|
+
if (node.index) {
|
|
125
|
+
const fromMeta = folderOrderMap.get(node.index.url);
|
|
126
|
+
if (fromMeta !== undefined) return fromMeta;
|
|
127
|
+
return pageOrderMap.get(node.index.url);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
120
130
|
return undefined;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
function sortNodes(nodes: Node[],
|
|
133
|
+
function sortNodes(nodes: Node[], pageOrderMap: Map<string, number>, folderOrderMap: Map<string, number>): Node[] {
|
|
124
134
|
return [...nodes]
|
|
125
135
|
.map(n =>
|
|
126
136
|
n.type === 'folder'
|
|
127
|
-
? ({ ...n, children: sortNodes(n.children,
|
|
137
|
+
? ({ ...n, children: sortNodes(n.children, pageOrderMap, folderOrderMap) } as Folder)
|
|
128
138
|
: n
|
|
129
139
|
)
|
|
130
140
|
.sort(
|
|
131
141
|
(a, b) =>
|
|
132
|
-
(getOrder(a,
|
|
133
|
-
(getOrder(b,
|
|
142
|
+
(getOrder(a, pageOrderMap, folderOrderMap) ?? Number.MAX_SAFE_INTEGER) -
|
|
143
|
+
(getOrder(b, pageOrderMap, folderOrderMap) ?? Number.MAX_SAFE_INTEGER)
|
|
134
144
|
);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
|
-
function
|
|
138
|
-
const
|
|
147
|
+
function buildFolderOrderMap(metaFiles: { path: string; data: Record<string, unknown> }[]): Map<string, number> {
|
|
148
|
+
const map = new Map<string, number>();
|
|
149
|
+
for (const meta of metaFiles) {
|
|
150
|
+
const order = meta.data.order as number | undefined;
|
|
151
|
+
if (order === undefined) continue;
|
|
152
|
+
const folderUrl = '/' + meta.path.replace(/\/meta\.json$/, '');
|
|
153
|
+
map.set(folderUrl, order);
|
|
154
|
+
}
|
|
155
|
+
return map;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sortTreeByOrder(tree: Root, pages: { url: string; data: unknown }[], metaFiles: { path: string; data: Record<string, unknown> }[]): Root {
|
|
159
|
+
const pageOrderMap = new Map<string, number>();
|
|
139
160
|
for (const page of pages) {
|
|
140
161
|
const d = page.data as Record<string, unknown>;
|
|
141
162
|
const order = d.order as number | undefined;
|
|
142
|
-
if (order !== undefined)
|
|
143
|
-
if (page.url === '/')
|
|
163
|
+
if (order !== undefined) pageOrderMap.set(page.url, order);
|
|
164
|
+
if (page.url === '/') pageOrderMap.set('/', order ?? 0);
|
|
144
165
|
}
|
|
145
|
-
|
|
166
|
+
const folderOrderMap = buildFolderOrderMap(metaFiles);
|
|
167
|
+
return { ...tree, children: sortNodes(tree.children, pageOrderMap, folderOrderMap) };
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
export async function getPageTree(): Promise<Root> {
|
|
171
|
+
if (cachedTree) return cachedTree;
|
|
149
172
|
const s = await getSource();
|
|
150
|
-
|
|
173
|
+
const metaFiles = buildFiles().filter(f => f.type === 'meta') as { path: string; data: Record<string, unknown> }[];
|
|
174
|
+
cachedTree = sortTreeByOrder(s.pageTree as Root, s.getPages(), metaFiles);
|
|
175
|
+
return cachedTree;
|
|
151
176
|
}
|
|
152
177
|
|
|
153
178
|
export async function getPages() {
|
|
@@ -186,12 +211,10 @@ function titleFromUrl(url: string): string {
|
|
|
186
211
|
.join(' ');
|
|
187
212
|
}
|
|
188
213
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
const
|
|
193
|
-
const i = pages.findIndex(p => p.url === url);
|
|
194
|
-
if (i < 0) return { prev: null, next: null };
|
|
214
|
+
async function getNavMap(): Promise<Map<string, PageNav>> {
|
|
215
|
+
if (cachedNavMap) return cachedNavMap;
|
|
216
|
+
const tree = await getPageTree();
|
|
217
|
+
const pages = flattenTree(tree.children);
|
|
195
218
|
const toLink = (p: (typeof pages)[number]): PageNavLink => ({
|
|
196
219
|
url: p.url,
|
|
197
220
|
title:
|
|
@@ -199,10 +222,21 @@ export async function getPageNav(slug: string[], tree?: Root): Promise<PageNav>
|
|
|
199
222
|
? p.name
|
|
200
223
|
: titleFromUrl(p.url)
|
|
201
224
|
});
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
225
|
+
const navMap = new Map<string, PageNav>();
|
|
226
|
+
for (let i = 0; i < pages.length; i++) {
|
|
227
|
+
navMap.set(pages[i].url, {
|
|
228
|
+
prev: i > 0 ? toLink(pages[i - 1]) : null,
|
|
229
|
+
next: i < pages.length - 1 ? toLink(pages[i + 1]) : null
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
cachedNavMap = navMap;
|
|
233
|
+
return cachedNavMap;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function getPageNav(slug: string[]): Promise<PageNav> {
|
|
237
|
+
const navMap = await getNavMap();
|
|
238
|
+
const url = slug.length === 0 ? '/' : `/${slug.join('/')}`;
|
|
239
|
+
return navMap.get(url) ?? { prev: null, next: null };
|
|
206
240
|
}
|
|
207
241
|
|
|
208
242
|
export function extractFrontmatter(page: { data: unknown }, fallbackTitle?: string): Frontmatter {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { useLocation } from 'react-router'
|
|
3
|
+
import { findApiOperation, type ApiRouteMatch } from '@/lib/api-routes'
|
|
4
|
+
import { usePageContext } from '@/lib/page-context'
|
|
5
|
+
|
|
6
|
+
export function useApiOperation(): ApiRouteMatch | null {
|
|
7
|
+
const { apiSpecs } = usePageContext()
|
|
8
|
+
const { pathname } = useLocation()
|
|
9
|
+
|
|
10
|
+
return useMemo(() => {
|
|
11
|
+
const slug = pathname.replace(/^\/apis\//, '').split('/').filter(Boolean)
|
|
12
|
+
if (slug.length !== 2) return null
|
|
13
|
+
return findApiOperation(apiSpecs, slug)
|
|
14
|
+
}, [apiSpecs, pathname])
|
|
15
|
+
}
|
package/src/pages/ApiPage.tsx
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { Flex, Headline, Text } from '@raystack/apsara';
|
|
2
1
|
import type { OpenAPIV3 } from 'openapi-types';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { Navigate } from 'react-router';
|
|
3
|
+
import { ApiOverview } from '@/components/api';
|
|
4
|
+
import { findApiOperation, getFirstApiUrl } from '@/lib/api-routes';
|
|
5
5
|
import { Head } from '@/lib/head';
|
|
6
|
-
import type { ApiSpec } from '@/lib/openapi';
|
|
7
6
|
import { usePageContext } from '@/lib/page-context';
|
|
8
7
|
|
|
9
8
|
interface ApiPageProps {
|
|
@@ -14,16 +13,9 @@ export function ApiPage({ slug }: ApiPageProps) {
|
|
|
14
13
|
const { config, apiSpecs } = usePageContext();
|
|
15
14
|
|
|
16
15
|
if (slug.length === 0) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
title='API Reference'
|
|
21
|
-
description={`API documentation for ${config.site.title}`}
|
|
22
|
-
config={config}
|
|
23
|
-
/>
|
|
24
|
-
<ApiLanding specs={apiSpecs} />
|
|
25
|
-
</>
|
|
26
|
-
);
|
|
16
|
+
const firstUrl = getFirstApiUrl(apiSpecs);
|
|
17
|
+
if (firstUrl) return <Navigate to={firstUrl} replace />;
|
|
18
|
+
return null;
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
const match = findApiOperation(apiSpecs, slug);
|
|
@@ -36,7 +28,7 @@ export function ApiPage({ slug }: ApiPageProps) {
|
|
|
36
28
|
return (
|
|
37
29
|
<>
|
|
38
30
|
<Head title={title} description={operation.description} config={config} />
|
|
39
|
-
<
|
|
31
|
+
<ApiOverview
|
|
40
32
|
method={match.method}
|
|
41
33
|
path={match.path}
|
|
42
34
|
operation={match.operation}
|
|
@@ -48,26 +40,3 @@ export function ApiPage({ slug }: ApiPageProps) {
|
|
|
48
40
|
);
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
function ApiLanding({ specs }: { specs: ApiSpec[] }) {
|
|
52
|
-
return (
|
|
53
|
-
<Flex
|
|
54
|
-
direction='column'
|
|
55
|
-
gap='large'
|
|
56
|
-
style={{ padding: 'var(--rs-space-7)' }}
|
|
57
|
-
>
|
|
58
|
-
<Headline size='medium' as='h1'>
|
|
59
|
-
API Reference
|
|
60
|
-
</Headline>
|
|
61
|
-
{specs.map(spec => (
|
|
62
|
-
<Flex key={spec.name} direction='column' gap='small'>
|
|
63
|
-
<Headline size='small' as='h2'>
|
|
64
|
-
{spec.name}
|
|
65
|
-
</Headline>
|
|
66
|
-
{spec.document.info.description && (
|
|
67
|
-
<Text size={3}>{spec.document.info.description}</Text>
|
|
68
|
-
)}
|
|
69
|
-
</Flex>
|
|
70
|
-
))}
|
|
71
|
-
</Flex>
|
|
72
|
-
);
|
|
73
|
-
}
|
package/src/pages/DocsPage.tsx
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
|
+
import { Navigate } from 'react-router';
|
|
1
2
|
import { Head } from '@/lib/head';
|
|
2
3
|
import { usePageContext } from '@/lib/page-context';
|
|
3
4
|
import { NotFound } from '@/pages/NotFound';
|
|
4
5
|
import { getTheme } from '@/themes/registry';
|
|
6
|
+
import type { Node } from 'fumadocs-core/page-tree';
|
|
7
|
+
|
|
8
|
+
function getFirstPageUrl(nodes: Node[]): string | null {
|
|
9
|
+
for (const node of nodes) {
|
|
10
|
+
if (node.type === 'page') return node.url;
|
|
11
|
+
if (node.type === 'folder') {
|
|
12
|
+
const url = getFirstPageUrl(node.children);
|
|
13
|
+
if (url) return url;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function findFolderFirstPage(nodes: Node[], pathname: string): string | null {
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
if (node.type === 'folder') {
|
|
22
|
+
const folderUrl = node.index?.url;
|
|
23
|
+
if (folderUrl === pathname) return getFirstPageUrl(node.children);
|
|
24
|
+
const found = findFolderFirstPage(node.children, pathname);
|
|
25
|
+
if (found) return found;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
interface DocsPageProps {
|
|
7
32
|
slug: string[];
|
|
@@ -10,7 +35,21 @@ interface DocsPageProps {
|
|
|
10
35
|
export function DocsPage({ slug }: DocsPageProps) {
|
|
11
36
|
const { config, tree, page, isLoading, errorStatus } = usePageContext();
|
|
12
37
|
|
|
13
|
-
if (errorStatus === 404)
|
|
38
|
+
if (errorStatus === 404) {
|
|
39
|
+
const pathname = `/${slug.join('/')}`;
|
|
40
|
+
const contentConfig = config.content?.find(c => c.dir === slug[0]);
|
|
41
|
+
const isContentRoot = slug.length === 1 && slug[0] === contentConfig?.dir;
|
|
42
|
+
if (contentConfig?.index_page) {
|
|
43
|
+
return <Navigate to={`/${contentConfig.dir}/${contentConfig.index_page}`} replace />;
|
|
44
|
+
}
|
|
45
|
+
if (isContentRoot) {
|
|
46
|
+
const firstUrl = getFirstPageUrl(tree.children);
|
|
47
|
+
if (firstUrl) return <Navigate to={firstUrl} replace />;
|
|
48
|
+
}
|
|
49
|
+
const folderFirstUrl = findFolderFirstPage(tree.children, pathname);
|
|
50
|
+
if (folderFirstUrl) return <Navigate to={folderFirstUrl} replace />;
|
|
51
|
+
return <NotFound />;
|
|
52
|
+
}
|
|
14
53
|
if (errorStatus) return <NotFound />;
|
|
15
54
|
const { Page, Skeleton } = getTheme(config.theme?.name);
|
|
16
55
|
|
|
@@ -51,10 +51,17 @@ export default defineHandler(async event => {
|
|
|
51
51
|
? await response.json()
|
|
52
52
|
: await response.text();
|
|
53
53
|
|
|
54
|
+
const sensitiveHeaders = new Set(['set-cookie', 'authorization', 'proxy-authorization', 'cookie']);
|
|
55
|
+
const responseHeaders: Record<string, string> = {};
|
|
56
|
+
response.headers.forEach((v, k) => {
|
|
57
|
+
if (!sensitiveHeaders.has(k.toLowerCase())) responseHeaders[k] = v;
|
|
58
|
+
});
|
|
59
|
+
|
|
54
60
|
return Response.json({
|
|
55
61
|
status: response.status,
|
|
56
62
|
statusText: response.statusText,
|
|
57
|
-
body: responseBody
|
|
63
|
+
body: responseBody,
|
|
64
|
+
headers: responseHeaders
|
|
58
65
|
});
|
|
59
66
|
} catch (error) {
|
|
60
67
|
const message =
|
|
@@ -19,7 +19,7 @@ import serverAssets from './entry-server?assets=ssr';
|
|
|
19
19
|
export default {
|
|
20
20
|
async fetch(req: Request) {
|
|
21
21
|
const url = new URL(req.url);
|
|
22
|
-
const pathname = url.pathname;
|
|
22
|
+
const pathname = decodeURIComponent(url.pathname);
|
|
23
23
|
|
|
24
24
|
const config = loadConfig();
|
|
25
25
|
const route = resolveRoute(pathname, config);
|
|
@@ -45,7 +45,7 @@ export default {
|
|
|
45
45
|
getPageTree(),
|
|
46
46
|
route.type === RouteType.DocsPage ? getPage(route.slug) : Promise.resolve(null),
|
|
47
47
|
]);
|
|
48
|
-
const nav = page ? await getPageNav(pageSlug
|
|
48
|
+
const nav = page ? await getPageNav(pageSlug) : { prev: null, next: null };
|
|
49
49
|
|
|
50
50
|
const relativePath = page ? getRelativePath(page) : null;
|
|
51
51
|
const originalPath = page ? getOriginalPath(page) : null;
|
|
@@ -7,6 +7,7 @@ import { safePath } from '@/server/utils/safe-path';
|
|
|
7
7
|
export default defineHandler(async event => {
|
|
8
8
|
const pathname = event.path || event.req.url?.split('?')[0] || '';
|
|
9
9
|
if (!pathname.endsWith('.md')) return;
|
|
10
|
+
if (pathname.startsWith('/apis/')) return;
|
|
10
11
|
|
|
11
12
|
const stripped = pathname.replace(/\.md$/, '');
|
|
12
13
|
const parts = stripped === '/index' || stripped === '/'
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
|
+
import { defineHandler, HTTPError } from 'nitro'
|
|
3
|
+
import { loadConfig } from '@/lib/config'
|
|
4
|
+
import { loadApiSpecs } from '@/lib/openapi'
|
|
5
|
+
import { findApiOperation } from '@/lib/api-routes'
|
|
6
|
+
import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema'
|
|
7
|
+
import { generateCurl } from '@/lib/snippet-generators'
|
|
8
|
+
|
|
9
|
+
export default defineHandler(async event => {
|
|
10
|
+
const pathname = event.path || event.req.url?.split('?')[0] || ''
|
|
11
|
+
if (!pathname.endsWith('.md')) return
|
|
12
|
+
|
|
13
|
+
const stripped = pathname.replace(/\.md$/, '').replace(/^\/apis\//, '')
|
|
14
|
+
const slug = stripped.split('/').filter(Boolean)
|
|
15
|
+
if (slug.length < 2) {
|
|
16
|
+
throw new HTTPError({ status: 404, message: 'Not Found' })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config = loadConfig()
|
|
20
|
+
const specs = await loadApiSpecs(config.api ?? [])
|
|
21
|
+
const match = findApiOperation(specs, slug)
|
|
22
|
+
|
|
23
|
+
if (!match) {
|
|
24
|
+
throw new HTTPError({ status: 404, message: 'Not Found' })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const md = generateApiMarkdown(match.method, match.path, match.operation, match.spec.server.url, match.spec.auth)
|
|
28
|
+
return new Response(md, { headers: { 'Content-Type': 'text/markdown; charset=utf-8' } })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
function generateApiMarkdown(
|
|
32
|
+
method: string,
|
|
33
|
+
path: string,
|
|
34
|
+
operation: OpenAPIV3.OperationObject,
|
|
35
|
+
serverUrl: string,
|
|
36
|
+
auth?: { type: string; header: string; placeholder?: string },
|
|
37
|
+
): string {
|
|
38
|
+
const lines: string[] = []
|
|
39
|
+
const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[]
|
|
40
|
+
|
|
41
|
+
lines.push(`# ${operation.summary ?? `${method} ${path}`}`)
|
|
42
|
+
lines.push('')
|
|
43
|
+
if (operation.description) {
|
|
44
|
+
lines.push(operation.description)
|
|
45
|
+
lines.push('')
|
|
46
|
+
}
|
|
47
|
+
lines.push(`\`${method}\` \`${path}\``)
|
|
48
|
+
lines.push('')
|
|
49
|
+
|
|
50
|
+
const headerParams = params.filter(p => p.in === 'header')
|
|
51
|
+
const pathParams = params.filter(p => p.in === 'path')
|
|
52
|
+
const queryParams = params.filter(p => p.in === 'query')
|
|
53
|
+
|
|
54
|
+
if (auth || headerParams.length > 0) {
|
|
55
|
+
lines.push('## Authorization')
|
|
56
|
+
lines.push('')
|
|
57
|
+
lines.push('| Header | Type | Required | Description |')
|
|
58
|
+
lines.push('| --- | --- | --- | --- |')
|
|
59
|
+
if (auth) {
|
|
60
|
+
lines.push(`| \`${auth.header}\` | string | Yes | ${auth.placeholder ?? 'API key'} |`)
|
|
61
|
+
}
|
|
62
|
+
for (const p of headerParams) {
|
|
63
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
|
|
64
|
+
lines.push(`| \`${p.name}\` | ${schema.type ?? 'string'} | ${p.required ? 'Yes' : 'No'} | ${p.description ?? ''} |`)
|
|
65
|
+
}
|
|
66
|
+
lines.push('')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (pathParams.length > 0) {
|
|
70
|
+
lines.push('## Path Parameters')
|
|
71
|
+
lines.push('')
|
|
72
|
+
lines.push('| Parameter | Type | Required | Description |')
|
|
73
|
+
lines.push('| --- | --- | --- | --- |')
|
|
74
|
+
for (const p of pathParams) {
|
|
75
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
|
|
76
|
+
lines.push(`| \`${p.name}\` | ${schema.type ?? 'string'} | ${p.required ? 'Yes' : 'No'} | ${p.description ?? ''} |`)
|
|
77
|
+
}
|
|
78
|
+
lines.push('')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (queryParams.length > 0) {
|
|
82
|
+
lines.push('## Query Parameters')
|
|
83
|
+
lines.push('')
|
|
84
|
+
lines.push('| Parameter | Type | Required | Description |')
|
|
85
|
+
lines.push('| --- | --- | --- | --- |')
|
|
86
|
+
for (const p of queryParams) {
|
|
87
|
+
const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject
|
|
88
|
+
lines.push(`| \`${p.name}\` | ${schema.type ?? 'string'} | ${p.required ? 'Yes' : 'No'} | ${p.description ?? ''} |`)
|
|
89
|
+
}
|
|
90
|
+
lines.push('')
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const requestBody = operation.requestBody as OpenAPIV3.RequestBodyObject | undefined
|
|
94
|
+
if (requestBody?.content) {
|
|
95
|
+
const contentType = Object.keys(requestBody.content)[0]
|
|
96
|
+
const schema = contentType ? requestBody.content[contentType]?.schema as OpenAPIV3.SchemaObject : undefined
|
|
97
|
+
if (schema) {
|
|
98
|
+
lines.push('## Request Body')
|
|
99
|
+
lines.push('')
|
|
100
|
+
lines.push(`Content-Type: \`${contentType}\``)
|
|
101
|
+
lines.push('')
|
|
102
|
+
const fields = flattenSchema(schema)
|
|
103
|
+
if (fields.length > 0) {
|
|
104
|
+
lines.push('| Field | Type | Required | Description |')
|
|
105
|
+
lines.push('| --- | --- | --- | --- |')
|
|
106
|
+
renderFieldTable(fields, lines, 0)
|
|
107
|
+
lines.push('')
|
|
108
|
+
}
|
|
109
|
+
const example = generateExampleJson(schema)
|
|
110
|
+
lines.push('**Example:**')
|
|
111
|
+
lines.push('')
|
|
112
|
+
lines.push('```json')
|
|
113
|
+
lines.push(JSON.stringify(example, null, 2))
|
|
114
|
+
lines.push('```')
|
|
115
|
+
lines.push('')
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const responses = operation.responses as Record<string, OpenAPIV3.ResponseObject> | undefined
|
|
120
|
+
if (responses) {
|
|
121
|
+
lines.push('## Responses')
|
|
122
|
+
lines.push('')
|
|
123
|
+
for (const [status, resp] of Object.entries(responses)) {
|
|
124
|
+
lines.push(`### ${status}${resp.description ? ` — ${resp.description}` : ''}`)
|
|
125
|
+
lines.push('')
|
|
126
|
+
const content = resp.content ?? {}
|
|
127
|
+
const contentType = Object.keys(content)[0]
|
|
128
|
+
const schema = contentType ? content[contentType]?.schema as OpenAPIV3.SchemaObject : undefined
|
|
129
|
+
if (schema) {
|
|
130
|
+
const fields = flattenSchema(schema)
|
|
131
|
+
if (fields.length > 0) {
|
|
132
|
+
lines.push('| Field | Type | Description |')
|
|
133
|
+
lines.push('| --- | --- | --- |')
|
|
134
|
+
renderResponseFieldTable(fields, lines, 0)
|
|
135
|
+
lines.push('')
|
|
136
|
+
}
|
|
137
|
+
const example = generateExampleJson(schema)
|
|
138
|
+
lines.push('```json')
|
|
139
|
+
lines.push(JSON.stringify(example, null, 2))
|
|
140
|
+
lines.push('```')
|
|
141
|
+
lines.push('')
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const headers: Record<string, string> = {}
|
|
147
|
+
if (auth) headers[auth.header] = auth.placeholder ?? 'YOUR_API_KEY'
|
|
148
|
+
if (requestBody?.content) {
|
|
149
|
+
const ct = Object.keys(requestBody.content)[0]
|
|
150
|
+
if (ct) headers['Content-Type'] = ct
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const bodySchema = requestBody?.content
|
|
154
|
+
? (Object.values(requestBody.content)[0]?.schema as OpenAPIV3.SchemaObject | undefined)
|
|
155
|
+
: undefined
|
|
156
|
+
const bodyStr = bodySchema ? JSON.stringify(generateExampleJson(bodySchema), null, 2) : undefined
|
|
157
|
+
|
|
158
|
+
lines.push('## cURL')
|
|
159
|
+
lines.push('')
|
|
160
|
+
lines.push('```bash')
|
|
161
|
+
lines.push(generateCurl({ method, url: serverUrl + path, headers, body: bodyStr }))
|
|
162
|
+
lines.push('```')
|
|
163
|
+
|
|
164
|
+
return lines.join('\n')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderFieldTable(fields: SchemaField[], lines: string[], depth: number) {
|
|
168
|
+
const indent = ' '.repeat(depth)
|
|
169
|
+
for (const f of fields) {
|
|
170
|
+
lines.push(`| ${indent}\`${f.name}\` | ${f.type} | ${f.required ? 'Yes' : 'No'} | ${f.description ?? ''} |`)
|
|
171
|
+
if (f.children) renderFieldTable(f.children, lines, depth + 1)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function renderResponseFieldTable(fields: SchemaField[], lines: string[], depth: number) {
|
|
176
|
+
const indent = ' '.repeat(depth)
|
|
177
|
+
for (const f of fields) {
|
|
178
|
+
lines.push(`| ${indent}\`${f.name}\` | ${f.type} | ${f.description ?? ''} |`)
|
|
179
|
+
if (f.children) renderResponseFieldTable(f.children, lines, depth + 1)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -48,6 +48,7 @@ export async function createViteConfig(
|
|
|
48
48
|
|
|
49
49
|
return {
|
|
50
50
|
root: packageRoot,
|
|
51
|
+
publicDir: path.resolve(projectRoot, 'public'),
|
|
51
52
|
configFile: false,
|
|
52
53
|
plugins: [
|
|
53
54
|
nitro({
|
|
@@ -131,6 +132,7 @@ export async function createViteConfig(
|
|
|
131
132
|
},
|
|
132
133
|
nitro: {
|
|
133
134
|
logLevel: 2,
|
|
135
|
+
publicAssets: [{ dir: path.resolve(projectRoot, 'public') }],
|
|
134
136
|
output: {
|
|
135
137
|
dir: resolveOutputDir(projectRoot, preset),
|
|
136
138
|
},
|