@raystack/chronicle 0.7.4 → 0.9.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 +14 -2
- package/package.json +3 -4
- 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/components/ui/search.module.css +27 -5
- package/src/components/ui/search.tsx +28 -19
- package/src/lib/api-routes.ts +37 -8
- package/src/lib/openapi.ts +26 -0
- package/src/lib/page-context.tsx +1 -1
- package/src/lib/schema.ts +45 -3
- package/src/lib/source.ts +79 -13
- 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/api/ready.ts +15 -0
- package/src/server/api/search.ts +159 -85
- package/src/server/entry-server.tsx +1 -1
- 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 +11 -0
- package/src/themes/default/Layout.module.css +53 -0
- package/src/themes/default/Layout.tsx +162 -11
- package/src/themes/default/Page.module.css +4 -0
- package/src/themes/default/Page.tsx +6 -1
- package/src/types/config.ts +2 -1
- 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/server/api/search.ts
CHANGED
|
@@ -1,79 +1,129 @@
|
|
|
1
|
-
import MiniSearch from 'minisearch';
|
|
2
1
|
import { defineHandler, HTTPError } from 'nitro';
|
|
2
|
+
import { useDatabase } from 'nitro/database';
|
|
3
3
|
import type { OpenAPIV3 } from 'openapi-types';
|
|
4
4
|
import { getSpecSlug } from '@/lib/api-routes';
|
|
5
5
|
import { getApiConfigsForVersion, loadConfig } from '@/lib/config';
|
|
6
6
|
import { loadApiSpecs } from '@/lib/openapi';
|
|
7
|
-
import { extractFrontmatter, getPagesForVersion } from '@/lib/source';
|
|
7
|
+
import { extractFrontmatter, getPageSearchContent, getPagesForVersion } from '@/lib/source';
|
|
8
8
|
import { LATEST_CONTEXT, type VersionContext } from '@/lib/version-source';
|
|
9
9
|
|
|
10
10
|
interface SearchDocument {
|
|
11
11
|
id: string;
|
|
12
12
|
url: string;
|
|
13
13
|
title: string;
|
|
14
|
-
|
|
14
|
+
headings: string;
|
|
15
|
+
body: string;
|
|
15
16
|
type: 'page' | 'api';
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import os from 'node:os';
|
|
22
|
+
import path from 'node:path';
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
const LOCK_FILE = path.join(os.tmpdir(), 'chronicle-search-ready');
|
|
25
|
+
|
|
26
|
+
export const indexedVersions = new Set<string>();
|
|
27
|
+
let indexPromise: Promise<void> | null = null;
|
|
28
|
+
|
|
29
|
+
function versionKey(ctx: VersionContext): string {
|
|
22
30
|
return ctx.dir ?? '__latest__';
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
// biome-ignore lint/suspicious/noEmptyBlockStatements: intentional no-op catch
|
|
34
|
+
fs.unlink(LOCK_FILE).catch(() => {});
|
|
35
|
+
|
|
36
|
+
export function isSearchReady(): boolean {
|
|
37
|
+
return existsSync(LOCK_FILE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function ensureIndex(ctx: VersionContext) {
|
|
41
|
+
const key = versionKey(ctx);
|
|
42
|
+
if (indexedVersions.has(key)) return;
|
|
43
|
+
if (indexPromise) return indexPromise;
|
|
44
|
+
indexPromise = buildIndex(ctx, key);
|
|
45
|
+
await indexPromise;
|
|
46
|
+
indexPromise = null;
|
|
47
|
+
await fs.writeFile(LOCK_FILE, new Date().toISOString());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function buildIndex(ctx: VersionContext, key: string) {
|
|
51
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: useDatabase is a Nitro DI accessor, not a React hook
|
|
52
|
+
const db = useDatabase();
|
|
53
|
+
|
|
54
|
+
await db.exec('DROP TABLE IF EXISTS search_fts');
|
|
55
|
+
await db.exec('DROP TABLE IF EXISTS search_docs');
|
|
56
|
+
|
|
57
|
+
await db.exec(`CREATE TABLE IF NOT EXISTS search_docs (
|
|
58
|
+
id TEXT PRIMARY KEY,
|
|
59
|
+
url TEXT NOT NULL,
|
|
60
|
+
title TEXT NOT NULL,
|
|
61
|
+
headings TEXT NOT NULL,
|
|
62
|
+
body TEXT NOT NULL,
|
|
63
|
+
type TEXT NOT NULL,
|
|
64
|
+
version TEXT NOT NULL
|
|
65
|
+
)`);
|
|
66
|
+
|
|
67
|
+
await db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
68
|
+
title,
|
|
69
|
+
headings,
|
|
70
|
+
body,
|
|
71
|
+
content=search_docs,
|
|
72
|
+
content_rowid=rowid
|
|
73
|
+
)`);
|
|
74
|
+
|
|
75
|
+
const docs = await buildDocs(ctx);
|
|
76
|
+
for (const doc of docs) {
|
|
77
|
+
await db.sql`INSERT INTO search_docs (id, url, title, headings, body, type, version)
|
|
78
|
+
VALUES (${doc.id}, ${doc.url}, ${doc.title}, ${doc.headings}, ${doc.body}, ${doc.type}, ${key})`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await db.sql`INSERT INTO search_fts (rowid, title, headings, body)
|
|
82
|
+
SELECT rowid, title, headings, body FROM search_docs WHERE version = ${key}`;
|
|
83
|
+
|
|
84
|
+
indexedVersions.add(key);
|
|
37
85
|
}
|
|
38
86
|
|
|
39
|
-
async function
|
|
87
|
+
async function buildDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
88
|
+
const docs: SearchDocument[] = [];
|
|
89
|
+
|
|
40
90
|
const pages = await getPagesForVersion(ctx);
|
|
41
|
-
|
|
91
|
+
for (const p of pages) {
|
|
42
92
|
const fm = extractFrontmatter(p);
|
|
43
|
-
|
|
93
|
+
const { headings, body } = await getPageSearchContent(p);
|
|
94
|
+
docs.push({
|
|
44
95
|
id: p.url,
|
|
45
96
|
url: p.url,
|
|
46
97
|
title: fm.title,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
98
|
+
headings,
|
|
99
|
+
body: [fm.description ?? '', body].join(' '),
|
|
100
|
+
type: 'page',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
52
103
|
|
|
53
|
-
async function buildApiDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
54
104
|
const config = loadConfig();
|
|
55
105
|
const apiConfigs = getApiConfigsForVersion(config, ctx.dir);
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
106
|
+
if (apiConfigs.length) {
|
|
107
|
+
const specs = await loadApiSpecs(apiConfigs);
|
|
108
|
+
for (const spec of specs) {
|
|
109
|
+
const specSlug = getSpecSlug(spec);
|
|
110
|
+
const paths = spec.document.paths ?? {};
|
|
111
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
112
|
+
if (!pathItem) continue;
|
|
113
|
+
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
114
|
+
const op = pathItem[method] as OpenAPIV3.OperationObject | undefined;
|
|
115
|
+
if (!op) continue;
|
|
116
|
+
const opId = op.operationId ?? `${method}_${pathStr.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`;
|
|
117
|
+
const url = `${ctx.urlPrefix}/apis/${specSlug}/${encodeURIComponent(opId)}`;
|
|
118
|
+
docs.push({
|
|
119
|
+
id: url,
|
|
120
|
+
url,
|
|
121
|
+
title: `${method.toUpperCase()} ${op.summary ?? opId}`,
|
|
122
|
+
headings: op.summary ?? opId,
|
|
123
|
+
body: [op.description ?? '', pathStr, method.toUpperCase()].join(' '),
|
|
124
|
+
type: 'api',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
77
127
|
}
|
|
78
128
|
}
|
|
79
129
|
}
|
|
@@ -81,27 +131,32 @@ async function buildApiDocs(ctx: VersionContext): Promise<SearchDocument[]> {
|
|
|
81
131
|
return docs;
|
|
82
132
|
}
|
|
83
133
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
docsCache.set(key, docs);
|
|
94
|
-
return docs;
|
|
95
|
-
}
|
|
134
|
+
function findMatch(
|
|
135
|
+
query: string,
|
|
136
|
+
title: string,
|
|
137
|
+
headings: string,
|
|
138
|
+
body: string,
|
|
139
|
+
): { match: 'title' | 'heading' | 'body'; snippet: string } {
|
|
140
|
+
if (title.toLowerCase().includes(query)) {
|
|
141
|
+
return { match: 'title', snippet: title };
|
|
142
|
+
}
|
|
96
143
|
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
144
|
+
const headingList = headings.split('\n').filter(Boolean);
|
|
145
|
+
for (const h of headingList) {
|
|
146
|
+
if (h.toLowerCase().includes(query)) {
|
|
147
|
+
return { match: 'heading', snippet: h };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const idx = body.toLowerCase().indexOf(query);
|
|
152
|
+
if (idx >= 0) {
|
|
153
|
+
const start = Math.max(0, idx - 40);
|
|
154
|
+
const end = Math.min(body.length, idx + query.length + 80);
|
|
155
|
+
const snippet = (start > 0 ? '...' : '') + body.slice(start, end).trim() + (end < body.length ? '...' : '');
|
|
156
|
+
return { match: 'body', snippet };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { match: 'title', snippet: title };
|
|
105
160
|
}
|
|
106
161
|
|
|
107
162
|
function resolveCtx(tag: string | null): VersionContext {
|
|
@@ -121,25 +176,44 @@ export default defineHandler(async event => {
|
|
|
121
176
|
const query = event.url.searchParams.get('query') ?? '';
|
|
122
177
|
const tag = event.url.searchParams.get('tag');
|
|
123
178
|
const ctx = resolveCtx(tag);
|
|
124
|
-
|
|
179
|
+
|
|
180
|
+
await ensureIndex(ctx);
|
|
181
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: useDatabase is a Nitro DI accessor, not a React hook
|
|
182
|
+
const db = useDatabase();
|
|
183
|
+
const key = versionKey(ctx);
|
|
125
184
|
|
|
126
185
|
if (!query) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})));
|
|
186
|
+
const result = await db.sql`SELECT id, url, title, type FROM search_docs
|
|
187
|
+
WHERE version = ${key} AND type = 'page'
|
|
188
|
+
LIMIT 8`;
|
|
189
|
+
return Response.json((result.rows ?? []).map(r => ({
|
|
190
|
+
id: r.id,
|
|
191
|
+
url: r.url,
|
|
192
|
+
type: r.type,
|
|
193
|
+
content: r.title,
|
|
194
|
+
})));
|
|
137
195
|
}
|
|
138
196
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
197
|
+
const searchTerm = query.split(/\s+/).map(t => `"${t}"*`).join(' ');
|
|
198
|
+
const result = await db.sql`SELECT s.id, s.url, s.title, s.headings, s.body, s.type,
|
|
199
|
+
bm25(search_fts, 10.0, 5.0, 1.0) AS score
|
|
200
|
+
FROM search_fts f
|
|
201
|
+
JOIN search_docs s ON s.rowid = f.rowid
|
|
202
|
+
WHERE search_fts MATCH ${searchTerm}
|
|
203
|
+
AND s.version = ${key}
|
|
204
|
+
ORDER BY score
|
|
205
|
+
LIMIT 20`;
|
|
206
|
+
|
|
207
|
+
const queryLower = query.toLowerCase();
|
|
208
|
+
return Response.json((result.rows ?? []).map(r => {
|
|
209
|
+
const { match, snippet } = findMatch(queryLower, r.title as string, r.headings as string, r.body as string);
|
|
210
|
+
return {
|
|
211
|
+
id: r.id,
|
|
212
|
+
url: r.url,
|
|
213
|
+
type: r.type,
|
|
214
|
+
content: r.title,
|
|
215
|
+
match,
|
|
216
|
+
snippet,
|
|
217
|
+
};
|
|
218
|
+
}));
|
|
145
219
|
});
|
|
@@ -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);
|
|
@@ -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,9 +132,19 @@ 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
|
},
|
|
139
|
+
experimental: {
|
|
140
|
+
database: true,
|
|
141
|
+
},
|
|
142
|
+
database: {
|
|
143
|
+
default: {
|
|
144
|
+
connector: 'sqlite',
|
|
145
|
+
options: { name: 'chronicle-search' },
|
|
146
|
+
},
|
|
147
|
+
},
|
|
137
148
|
},
|
|
138
149
|
};
|
|
139
150
|
}
|
|
@@ -226,3 +226,56 @@
|
|
|
226
226
|
.page {
|
|
227
227
|
padding: var(--rs-space-2) 0;
|
|
228
228
|
}
|
|
229
|
+
|
|
230
|
+
.apiGroup {
|
|
231
|
+
margin-top: var(--rs-space-8);
|
|
232
|
+
width: 100%;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.apiGroup:first-child {
|
|
236
|
+
margin-top: 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.apiGroupLabel {
|
|
240
|
+
font-size: var(--rs-font-size-small);
|
|
241
|
+
font-weight: var(--rs-font-weight-medium);
|
|
242
|
+
line-height: var(--rs-line-height-small);
|
|
243
|
+
letter-spacing: var(--rs-letter-spacing-small);
|
|
244
|
+
color: var(--rs-color-foreground-base-secondary);
|
|
245
|
+
padding: 0 var(--rs-space-3);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.apiItem {
|
|
249
|
+
padding: var(--rs-space-3);
|
|
250
|
+
border-radius: var(--rs-radius-2);
|
|
251
|
+
text-decoration: none;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
white-space: nowrap;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.apiItem:hover {
|
|
257
|
+
background: var(--rs-color-background-neutral-secondary);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.apiItemActive {
|
|
261
|
+
background: var(--rs-color-background-neutral-secondary);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.apiItemName {
|
|
265
|
+
flex: 1;
|
|
266
|
+
min-width: 0;
|
|
267
|
+
overflow: hidden;
|
|
268
|
+
text-overflow: ellipsis;
|
|
269
|
+
font-size: var(--rs-font-size-small);
|
|
270
|
+
font-weight: var(--rs-font-weight-medium);
|
|
271
|
+
line-height: var(--rs-line-height-small);
|
|
272
|
+
letter-spacing: var(--rs-letter-spacing-small);
|
|
273
|
+
color: var(--rs-color-foreground-base-primary);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.apiMethodText {
|
|
277
|
+
font-family: var(--rs-font-mono);
|
|
278
|
+
font-size: var(--rs-font-size-mono-mini);
|
|
279
|
+
line-height: var(--rs-line-height-mini);
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
}
|