@raystack/chronicle 0.1.0-canary.111b55a
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/bin/chronicle.js +2 -0
- package/dist/cli/index.js +963 -0
- package/package.json +67 -0
- package/src/cli/__tests__/config.test.ts +25 -0
- package/src/cli/__tests__/scaffold.test.ts +10 -0
- package/src/cli/commands/build.ts +74 -0
- package/src/cli/commands/dev.ts +21 -0
- package/src/cli/commands/init.ts +154 -0
- package/src/cli/commands/serve.ts +55 -0
- package/src/cli/commands/start.ts +24 -0
- package/src/cli/index.ts +21 -0
- package/src/cli/utils/config.ts +43 -0
- package/src/cli/utils/index.ts +2 -0
- package/src/cli/utils/resolve.ts +6 -0
- package/src/cli/utils/scaffold.ts +20 -0
- package/src/components/api/code-snippets.module.css +7 -0
- package/src/components/api/code-snippets.tsx +76 -0
- package/src/components/api/endpoint-page.module.css +58 -0
- package/src/components/api/endpoint-page.tsx +283 -0
- package/src/components/api/field-row.module.css +126 -0
- package/src/components/api/field-row.tsx +204 -0
- package/src/components/api/field-section.module.css +24 -0
- package/src/components/api/field-section.tsx +100 -0
- package/src/components/api/index.ts +8 -0
- package/src/components/api/json-editor.module.css +9 -0
- package/src/components/api/json-editor.tsx +61 -0
- package/src/components/api/key-value-editor.module.css +13 -0
- package/src/components/api/key-value-editor.tsx +62 -0
- package/src/components/api/method-badge.module.css +4 -0
- package/src/components/api/method-badge.tsx +29 -0
- package/src/components/api/response-panel.module.css +8 -0
- package/src/components/api/response-panel.tsx +44 -0
- package/src/components/common/breadcrumb.tsx +3 -0
- package/src/components/common/button.tsx +3 -0
- package/src/components/common/callout.module.css +7 -0
- package/src/components/common/callout.tsx +27 -0
- package/src/components/common/code-block.tsx +3 -0
- package/src/components/common/dialog.tsx +3 -0
- package/src/components/common/index.ts +10 -0
- package/src/components/common/input-field.tsx +3 -0
- package/src/components/common/sidebar.tsx +3 -0
- package/src/components/common/switch.tsx +3 -0
- package/src/components/common/table.tsx +3 -0
- package/src/components/common/tabs.tsx +3 -0
- package/src/components/mdx/code.module.css +42 -0
- package/src/components/mdx/code.tsx +36 -0
- package/src/components/mdx/details.module.css +14 -0
- package/src/components/mdx/details.tsx +17 -0
- package/src/components/mdx/image.tsx +24 -0
- package/src/components/mdx/index.tsx +35 -0
- package/src/components/mdx/link.tsx +37 -0
- package/src/components/mdx/mermaid.module.css +9 -0
- package/src/components/mdx/mermaid.tsx +37 -0
- package/src/components/mdx/paragraph.module.css +8 -0
- package/src/components/mdx/paragraph.tsx +19 -0
- package/src/components/mdx/table.tsx +40 -0
- package/src/components/ui/breadcrumbs.tsx +72 -0
- package/src/components/ui/client-theme-switcher.tsx +18 -0
- package/src/components/ui/footer.module.css +27 -0
- package/src/components/ui/footer.tsx +31 -0
- package/src/components/ui/search.module.css +111 -0
- package/src/components/ui/search.tsx +173 -0
- package/src/lib/api-routes.ts +120 -0
- package/src/lib/config.ts +56 -0
- package/src/lib/head.tsx +45 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/openapi.ts +188 -0
- package/src/lib/page-context.tsx +95 -0
- package/src/lib/remark-unused-directives.ts +30 -0
- package/src/lib/schema.ts +99 -0
- package/src/lib/snippet-generators.ts +87 -0
- package/src/lib/source.ts +138 -0
- package/src/pages/ApiLayout.module.css +22 -0
- package/src/pages/ApiLayout.tsx +29 -0
- package/src/pages/ApiPage.tsx +68 -0
- package/src/pages/DocsLayout.tsx +18 -0
- package/src/pages/DocsPage.tsx +43 -0
- package/src/pages/NotFound.tsx +10 -0
- package/src/pages/__tests__/head.test.tsx +57 -0
- package/src/server/App.tsx +59 -0
- package/src/server/__tests__/entry-server.test.tsx +35 -0
- package/src/server/__tests__/handlers.test.ts +77 -0
- package/src/server/__tests__/og.test.ts +23 -0
- package/src/server/__tests__/router.test.ts +72 -0
- package/src/server/__tests__/vite-config.test.ts +25 -0
- package/src/server/adapters/vercel.ts +133 -0
- package/src/server/build-search-index.ts +107 -0
- package/src/server/dev.ts +156 -0
- package/src/server/entry-client.tsx +74 -0
- package/src/server/entry-prod.ts +97 -0
- package/src/server/entry-server.tsx +35 -0
- package/src/server/entry-vercel.ts +28 -0
- package/src/server/handlers/apis-proxy.ts +52 -0
- package/src/server/handlers/health.ts +3 -0
- package/src/server/handlers/llms.ts +58 -0
- package/src/server/handlers/og.ts +87 -0
- package/src/server/handlers/robots.ts +11 -0
- package/src/server/handlers/search.ts +172 -0
- package/src/server/handlers/sitemap.ts +39 -0
- package/src/server/handlers/specs.ts +9 -0
- package/src/server/index.html +12 -0
- package/src/server/prod.ts +18 -0
- package/src/server/request-handler.ts +63 -0
- package/src/server/router.ts +42 -0
- package/src/server/vite-config.ts +71 -0
- package/src/themes/default/Layout.module.css +81 -0
- package/src/themes/default/Layout.tsx +132 -0
- package/src/themes/default/Page.module.css +106 -0
- package/src/themes/default/Page.tsx +21 -0
- package/src/themes/default/Toc.module.css +48 -0
- package/src/themes/default/Toc.tsx +66 -0
- package/src/themes/default/font.ts +4 -0
- package/src/themes/default/index.ts +13 -0
- package/src/themes/paper/ChapterNav.module.css +71 -0
- package/src/themes/paper/ChapterNav.tsx +95 -0
- package/src/themes/paper/Layout.module.css +33 -0
- package/src/themes/paper/Layout.tsx +25 -0
- package/src/themes/paper/Page.module.css +174 -0
- package/src/themes/paper/Page.tsx +106 -0
- package/src/themes/paper/ReadingProgress.module.css +132 -0
- package/src/themes/paper/ReadingProgress.tsx +294 -0
- package/src/themes/paper/index.ts +8 -0
- package/src/themes/registry.ts +14 -0
- package/src/types/config.ts +80 -0
- package/src/types/content.ts +36 -0
- package/src/types/index.ts +3 -0
- package/src/types/theme.ts +22 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { parse } from 'yaml'
|
|
4
|
+
import type { ChronicleConfig } from '@/types'
|
|
5
|
+
|
|
6
|
+
const CONFIG_FILE = 'chronicle.yaml'
|
|
7
|
+
|
|
8
|
+
const defaultConfig: ChronicleConfig = {
|
|
9
|
+
title: 'Documentation',
|
|
10
|
+
theme: { name: 'default' },
|
|
11
|
+
search: { enabled: true, placeholder: 'Search...' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveConfigPath(): string | null {
|
|
15
|
+
// Check project root via env var
|
|
16
|
+
const projectRoot = process.env.CHRONICLE_PROJECT_ROOT
|
|
17
|
+
if (projectRoot) {
|
|
18
|
+
const rootPath = path.join(projectRoot, CONFIG_FILE)
|
|
19
|
+
if (fs.existsSync(rootPath)) return rootPath
|
|
20
|
+
}
|
|
21
|
+
// Check cwd
|
|
22
|
+
const cwdPath = path.join(process.cwd(), CONFIG_FILE)
|
|
23
|
+
if (fs.existsSync(cwdPath)) return cwdPath
|
|
24
|
+
// Check content dir
|
|
25
|
+
const contentDir = process.env.CHRONICLE_CONTENT_DIR
|
|
26
|
+
if (contentDir) {
|
|
27
|
+
const contentPath = path.join(contentDir, CONFIG_FILE)
|
|
28
|
+
if (fs.existsSync(contentPath)) return contentPath
|
|
29
|
+
}
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadConfig(): ChronicleConfig {
|
|
34
|
+
const configPath = resolveConfigPath()
|
|
35
|
+
|
|
36
|
+
if (!configPath) {
|
|
37
|
+
return defaultConfig
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const raw = fs.readFileSync(configPath, 'utf-8')
|
|
41
|
+
const userConfig = parse(raw) as Partial<ChronicleConfig>
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...defaultConfig,
|
|
45
|
+
...userConfig,
|
|
46
|
+
theme: {
|
|
47
|
+
name: userConfig.theme?.name ?? defaultConfig.theme!.name,
|
|
48
|
+
colors: { ...defaultConfig.theme?.colors, ...userConfig.theme?.colors },
|
|
49
|
+
},
|
|
50
|
+
search: { ...defaultConfig.search, ...userConfig.search },
|
|
51
|
+
footer: userConfig.footer,
|
|
52
|
+
api: userConfig.api,
|
|
53
|
+
llms: { enabled: false, ...userConfig.llms },
|
|
54
|
+
analytics: { enabled: false, ...userConfig.analytics },
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/lib/head.tsx
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ChronicleConfig } from '@/types'
|
|
2
|
+
|
|
3
|
+
export interface HeadProps {
|
|
4
|
+
title: string
|
|
5
|
+
description?: string
|
|
6
|
+
config: ChronicleConfig
|
|
7
|
+
jsonLd?: Record<string, unknown>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Head({ title, description, config, jsonLd }: HeadProps) {
|
|
11
|
+
const fullTitle = `${title} | ${config.title}`
|
|
12
|
+
const ogParams = new URLSearchParams({ title })
|
|
13
|
+
if (description) ogParams.set('description', description)
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<title>{fullTitle}</title>
|
|
18
|
+
{description && <meta name="description" content={description} />}
|
|
19
|
+
|
|
20
|
+
{config.url && (
|
|
21
|
+
<>
|
|
22
|
+
<meta property="og:title" content={title} />
|
|
23
|
+
{description && <meta property="og:description" content={description} />}
|
|
24
|
+
<meta property="og:site_name" content={config.title} />
|
|
25
|
+
<meta property="og:type" content="website" />
|
|
26
|
+
<meta property="og:image" content={`/og?${ogParams.toString()}`} />
|
|
27
|
+
<meta property="og:image:width" content="1200" />
|
|
28
|
+
<meta property="og:image:height" content="630" />
|
|
29
|
+
|
|
30
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
31
|
+
<meta name="twitter:title" content={title} />
|
|
32
|
+
{description && <meta name="twitter:description" content={description} />}
|
|
33
|
+
<meta name="twitter:image" content={`/og?${ogParams.toString()}`} />
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
|
|
37
|
+
{jsonLd && (
|
|
38
|
+
<script
|
|
39
|
+
type="application/ld+json"
|
|
40
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd, null, 2) }}
|
|
41
|
+
/>
|
|
42
|
+
)}
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { parse as parseYaml } from 'yaml'
|
|
4
|
+
import type { OpenAPIV2, OpenAPIV3 } from 'openapi-types'
|
|
5
|
+
import type { ApiConfig, ApiServerConfig, ApiAuthConfig } from '@/types/config'
|
|
6
|
+
|
|
7
|
+
type JsonObject = Record<string, unknown>
|
|
8
|
+
|
|
9
|
+
export interface ApiSpec {
|
|
10
|
+
name: string
|
|
11
|
+
basePath: string
|
|
12
|
+
server: ApiServerConfig
|
|
13
|
+
auth?: ApiAuthConfig
|
|
14
|
+
document: OpenAPIV3.Document
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type { SchemaField } from './schema'
|
|
18
|
+
export { flattenSchema } from './schema'
|
|
19
|
+
|
|
20
|
+
export function loadApiSpecs(apiConfigs: ApiConfig[]): ApiSpec[] {
|
|
21
|
+
const contentDir = process.env.CHRONICLE_CONTENT_DIR ?? process.cwd()
|
|
22
|
+
return apiConfigs.map((config) => loadApiSpec(config, contentDir))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadApiSpec(config: ApiConfig, contentDir: string): ApiSpec {
|
|
26
|
+
const specPath = path.resolve(contentDir, config.spec)
|
|
27
|
+
const raw = fs.readFileSync(specPath, 'utf-8')
|
|
28
|
+
const isYaml = specPath.endsWith('.yaml') || specPath.endsWith('.yml')
|
|
29
|
+
const doc = (isYaml ? parseYaml(raw) : JSON.parse(raw)) as OpenAPIV2.Document | OpenAPIV3.Document
|
|
30
|
+
|
|
31
|
+
let v3Doc: OpenAPIV3.Document
|
|
32
|
+
|
|
33
|
+
if ('swagger' in doc && doc.swagger === '2.0') {
|
|
34
|
+
v3Doc = convertV2toV3(doc as OpenAPIV2.Document)
|
|
35
|
+
} else if ('openapi' in doc && doc.openapi.startsWith('3.')) {
|
|
36
|
+
v3Doc = resolveDocument(doc as OpenAPIV3.Document)
|
|
37
|
+
} else {
|
|
38
|
+
throw new Error(`Unsupported spec version in ${config.spec}`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: config.name,
|
|
43
|
+
basePath: config.basePath,
|
|
44
|
+
server: config.server,
|
|
45
|
+
auth: config.auth,
|
|
46
|
+
document: v3Doc,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- $ref resolution ---
|
|
51
|
+
|
|
52
|
+
function resolveRef(ref: string, root: JsonObject): JsonObject {
|
|
53
|
+
const parts = ref.replace(/^#\//, '').split('/')
|
|
54
|
+
let current: unknown = root
|
|
55
|
+
for (const part of parts) {
|
|
56
|
+
if (current && typeof current === 'object' && !Array.isArray(current)) {
|
|
57
|
+
current = (current as JsonObject)[part]
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`Cannot resolve $ref: ${ref}`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return current as JsonObject
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function deepResolveRefs(
|
|
66
|
+
obj: unknown,
|
|
67
|
+
root: JsonObject,
|
|
68
|
+
stack = new Set<string>(),
|
|
69
|
+
cache = new Map<string, JsonObject>(),
|
|
70
|
+
): unknown {
|
|
71
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') return obj
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(obj)) {
|
|
74
|
+
return obj.map((item) => deepResolveRefs(item, root, stack, cache))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const record = obj as JsonObject
|
|
78
|
+
|
|
79
|
+
if (typeof record.$ref === 'string') {
|
|
80
|
+
const ref = record.$ref
|
|
81
|
+
if (cache.has(ref)) return cache.get(ref) as JsonObject
|
|
82
|
+
if (stack.has(ref)) return { type: 'object', description: '[circular]' }
|
|
83
|
+
stack.add(ref)
|
|
84
|
+
const resolved = deepResolveRefs(resolveRef(ref, root), root, stack, cache) as JsonObject
|
|
85
|
+
stack.delete(ref)
|
|
86
|
+
cache.set(ref, resolved)
|
|
87
|
+
return resolved
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result: JsonObject = {}
|
|
91
|
+
for (const [key, value] of Object.entries(record)) {
|
|
92
|
+
result[key] = deepResolveRefs(value, root, stack, cache)
|
|
93
|
+
}
|
|
94
|
+
return result
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveDocument(doc: OpenAPIV3.Document): OpenAPIV3.Document {
|
|
98
|
+
const root = doc as unknown as JsonObject
|
|
99
|
+
return deepResolveRefs(doc, root) as unknown as OpenAPIV3.Document
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- V2 → V3 conversion ---
|
|
103
|
+
|
|
104
|
+
function convertV2toV3(doc: OpenAPIV2.Document): OpenAPIV3.Document {
|
|
105
|
+
const root = doc as unknown as JsonObject
|
|
106
|
+
const resolved = deepResolveRefs(doc, root) as unknown as OpenAPIV2.Document
|
|
107
|
+
|
|
108
|
+
const v3Paths: OpenAPIV3.PathsObject = {}
|
|
109
|
+
|
|
110
|
+
for (const [pathStr, pathItem] of Object.entries(resolved.paths ?? {})) {
|
|
111
|
+
if (!pathItem) continue
|
|
112
|
+
const v3PathItem: OpenAPIV3.PathItemObject = {}
|
|
113
|
+
|
|
114
|
+
for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) {
|
|
115
|
+
const op = (pathItem as Record<string, unknown>)[method] as OpenAPIV2.OperationObject | undefined
|
|
116
|
+
if (!op) continue
|
|
117
|
+
v3PathItem[method] = convertV2Operation(op)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
v3Paths[pathStr] = v3PathItem
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
openapi: '3.0.0',
|
|
125
|
+
info: resolved.info as unknown as OpenAPIV3.InfoObject,
|
|
126
|
+
paths: v3Paths,
|
|
127
|
+
tags: (resolved.tags ?? []) as unknown as OpenAPIV3.TagObject[],
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function convertV2Operation(op: OpenAPIV2.OperationObject): OpenAPIV3.OperationObject {
|
|
132
|
+
const params = (op.parameters ?? []) as OpenAPIV2.Parameter[]
|
|
133
|
+
|
|
134
|
+
const v3Params: OpenAPIV3.ParameterObject[] = params
|
|
135
|
+
.filter((p) => p.in !== 'body')
|
|
136
|
+
.map((p) => ({
|
|
137
|
+
name: p.name,
|
|
138
|
+
in: p.in as 'path' | 'query' | 'header' | 'cookie',
|
|
139
|
+
required: p.required ?? false,
|
|
140
|
+
description: p.description,
|
|
141
|
+
schema: { type: ((p as JsonObject).type as string) ?? 'string', format: (p as JsonObject).format as string | undefined } as OpenAPIV3.SchemaObject,
|
|
142
|
+
}))
|
|
143
|
+
|
|
144
|
+
const bodyParam = params.find((p) => p.in === 'body') as JsonObject | undefined
|
|
145
|
+
let requestBody: OpenAPIV3.RequestBodyObject | undefined
|
|
146
|
+
if (bodyParam?.schema) {
|
|
147
|
+
requestBody = {
|
|
148
|
+
required: (bodyParam.required as boolean) ?? false,
|
|
149
|
+
content: {
|
|
150
|
+
'application/json': {
|
|
151
|
+
schema: bodyParam.schema as OpenAPIV3.SchemaObject,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const v3Responses: OpenAPIV3.ResponsesObject = {}
|
|
158
|
+
for (const [status, resp] of Object.entries(op.responses ?? {})) {
|
|
159
|
+
const v2Resp = resp as OpenAPIV2.ResponseObject
|
|
160
|
+
const v3Resp: OpenAPIV3.ResponseObject = {
|
|
161
|
+
description: v2Resp.description ?? '',
|
|
162
|
+
}
|
|
163
|
+
if ((v2Resp as unknown as JsonObject).schema) {
|
|
164
|
+
v3Resp.content = {
|
|
165
|
+
'application/json': {
|
|
166
|
+
schema: (v2Resp as unknown as JsonObject).schema as OpenAPIV3.SchemaObject,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
v3Responses[status] = v3Resp
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result: OpenAPIV3.OperationObject = {
|
|
174
|
+
operationId: op.operationId,
|
|
175
|
+
summary: op.summary,
|
|
176
|
+
description: op.description,
|
|
177
|
+
tags: op.tags,
|
|
178
|
+
parameters: v3Params,
|
|
179
|
+
responses: v3Responses,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (requestBody) {
|
|
183
|
+
result.requestBody = requestBody
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
|
|
2
|
+
import { useLocation } from 'react-router-dom'
|
|
3
|
+
import type { ChronicleConfig, Frontmatter, PageTree } from '@/types'
|
|
4
|
+
import type { ApiSpec } from '@/lib/openapi'
|
|
5
|
+
import { getPage, loadPageComponent, buildPageTree } from '@/lib/source'
|
|
6
|
+
import { mdxComponents } from '@/components/mdx'
|
|
7
|
+
import React from 'react'
|
|
8
|
+
|
|
9
|
+
interface PageData {
|
|
10
|
+
slug: string[]
|
|
11
|
+
frontmatter: Frontmatter
|
|
12
|
+
content: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PageContextValue {
|
|
16
|
+
config: ChronicleConfig
|
|
17
|
+
tree: PageTree
|
|
18
|
+
page: PageData | null
|
|
19
|
+
apiSpecs: ApiSpec[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PageContext = createContext<PageContextValue | null>(null)
|
|
23
|
+
|
|
24
|
+
export function usePageContext(): PageContextValue {
|
|
25
|
+
const ctx = useContext(PageContext)
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
console.error('usePageContext: no context found!')
|
|
28
|
+
return {
|
|
29
|
+
config: { title: 'Documentation' },
|
|
30
|
+
tree: { name: 'root', children: [] },
|
|
31
|
+
page: null,
|
|
32
|
+
apiSpecs: [],
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return ctx
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PageProviderProps {
|
|
39
|
+
initialConfig: ChronicleConfig
|
|
40
|
+
initialTree: PageTree
|
|
41
|
+
initialPage: PageData | null
|
|
42
|
+
initialApiSpecs: ApiSpec[]
|
|
43
|
+
children: ReactNode
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function PageProvider({ initialConfig, initialTree, initialPage, initialApiSpecs, children }: PageProviderProps) {
|
|
47
|
+
const { pathname } = useLocation()
|
|
48
|
+
const [tree, setTree] = useState<PageTree>(initialTree)
|
|
49
|
+
const [page, setPage] = useState<PageData | null>(initialPage)
|
|
50
|
+
const [apiSpecs, setApiSpecs] = useState<ApiSpec[]>(initialApiSpecs)
|
|
51
|
+
const [currentPath, setCurrentPath] = useState(pathname)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (pathname === currentPath) return
|
|
55
|
+
setCurrentPath(pathname)
|
|
56
|
+
|
|
57
|
+
let cancelled = false
|
|
58
|
+
|
|
59
|
+
if (pathname.startsWith('/apis')) {
|
|
60
|
+
// Fetch API specs if not already loaded
|
|
61
|
+
if (apiSpecs.length === 0) {
|
|
62
|
+
fetch('/api/specs')
|
|
63
|
+
.then((res) => res.json())
|
|
64
|
+
.then((specs) => { if (!cancelled) setApiSpecs(specs) })
|
|
65
|
+
.catch(() => {})
|
|
66
|
+
}
|
|
67
|
+
return () => { cancelled = true }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function load() {
|
|
71
|
+
const slug = pathname === '/' ? [] : pathname.slice(1).split('/').filter(Boolean)
|
|
72
|
+
|
|
73
|
+
const [sourcePage, newTree] = await Promise.all([getPage(slug), buildPageTree()])
|
|
74
|
+
if (cancelled || !sourcePage) return
|
|
75
|
+
|
|
76
|
+
const component = await loadPageComponent(sourcePage)
|
|
77
|
+
if (cancelled) return
|
|
78
|
+
|
|
79
|
+
setTree(newTree)
|
|
80
|
+
setPage({
|
|
81
|
+
slug,
|
|
82
|
+
frontmatter: sourcePage.frontmatter,
|
|
83
|
+
content: component ? React.createElement(component, { components: mdxComponents }) : null,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
load()
|
|
87
|
+
return () => { cancelled = true }
|
|
88
|
+
}, [pathname])
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<PageContext.Provider value={{ config: initialConfig, tree, page, apiSpecs }}>
|
|
92
|
+
{children}
|
|
93
|
+
</PageContext.Provider>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit'
|
|
2
|
+
import type { Plugin } from 'unified'
|
|
3
|
+
import type { Node } from 'unist'
|
|
4
|
+
|
|
5
|
+
const remarkUnusedDirectives: Plugin = () => {
|
|
6
|
+
return (tree) => {
|
|
7
|
+
visit(tree, ['textDirective'], (node) => {
|
|
8
|
+
const directive = node as Node & {
|
|
9
|
+
name?: string
|
|
10
|
+
attributes?: Record<string, string>
|
|
11
|
+
children?: Node[]
|
|
12
|
+
value?: string
|
|
13
|
+
[key: string]: unknown
|
|
14
|
+
}
|
|
15
|
+
if (!directive.data) {
|
|
16
|
+
const hasAttributes = directive.attributes && Object.keys(directive.attributes).length > 0
|
|
17
|
+
const hasChildren = directive.children && directive.children.length > 0
|
|
18
|
+
if (!hasAttributes && !hasChildren) {
|
|
19
|
+
const name = directive.name
|
|
20
|
+
if (!name) return
|
|
21
|
+
Object.keys(directive).forEach((key) => delete directive[key])
|
|
22
|
+
directive.type = 'text'
|
|
23
|
+
directive.value = `:${name}`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default remarkUnusedDirectives
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { OpenAPIV3 } from 'openapi-types'
|
|
2
|
+
|
|
3
|
+
export interface SchemaField {
|
|
4
|
+
name: string
|
|
5
|
+
type: string
|
|
6
|
+
required: boolean
|
|
7
|
+
description?: string
|
|
8
|
+
default?: unknown
|
|
9
|
+
enum?: unknown[]
|
|
10
|
+
children?: SchemaField[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function flattenSchema(
|
|
14
|
+
schema: OpenAPIV3.SchemaObject,
|
|
15
|
+
requiredFields: string[] = [],
|
|
16
|
+
): SchemaField[] {
|
|
17
|
+
if (schema.type === 'array' && schema.items) {
|
|
18
|
+
const items = schema.items as OpenAPIV3.SchemaObject
|
|
19
|
+
const itemType = inferType(items)
|
|
20
|
+
const children =
|
|
21
|
+
itemType === 'object' || items.properties
|
|
22
|
+
? flattenSchema(items, items.required ?? [])
|
|
23
|
+
: itemType.endsWith('[]') && (items as OpenAPIV3.ArraySchemaObject).items
|
|
24
|
+
? flattenSchema((items as OpenAPIV3.ArraySchemaObject).items as OpenAPIV3.SchemaObject)
|
|
25
|
+
: undefined
|
|
26
|
+
return [{
|
|
27
|
+
name: 'items',
|
|
28
|
+
type: `${itemType}[]`,
|
|
29
|
+
required: true,
|
|
30
|
+
description: items.description,
|
|
31
|
+
children: children?.length ? children : undefined,
|
|
32
|
+
}]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (schema.type === 'object' || schema.properties) {
|
|
36
|
+
const properties = (schema.properties ?? {}) as Record<string, OpenAPIV3.SchemaObject>
|
|
37
|
+
const required = schema.required ?? requiredFields
|
|
38
|
+
|
|
39
|
+
return Object.entries(properties).map(([name, prop]) => {
|
|
40
|
+
const fieldType = inferType(prop)
|
|
41
|
+
const children =
|
|
42
|
+
fieldType === 'object' || prop.properties
|
|
43
|
+
? flattenSchema(prop, prop.required)
|
|
44
|
+
: fieldType.endsWith('[]') && (prop as OpenAPIV3.ArraySchemaObject).items
|
|
45
|
+
? flattenSchema((prop as OpenAPIV3.ArraySchemaObject).items as OpenAPIV3.SchemaObject)
|
|
46
|
+
: undefined
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
type: fieldType,
|
|
51
|
+
required: required.includes(name),
|
|
52
|
+
description: prop.description,
|
|
53
|
+
default: prop.default,
|
|
54
|
+
enum: prop.enum,
|
|
55
|
+
children: children?.length ? children : undefined,
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return []
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function generateExampleJson(schema: OpenAPIV3.SchemaObject): unknown {
|
|
64
|
+
if (schema.example !== undefined) return schema.example
|
|
65
|
+
if (schema.default !== undefined) return schema.default
|
|
66
|
+
|
|
67
|
+
if (schema.type === 'array') {
|
|
68
|
+
const items = schema.items as OpenAPIV3.SchemaObject | undefined
|
|
69
|
+
return items ? [generateExampleJson(items)] : []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (schema.type === 'object' || schema.properties) {
|
|
73
|
+
const properties = (schema.properties ?? {}) as Record<string, OpenAPIV3.SchemaObject>
|
|
74
|
+
const result: Record<string, unknown> = {}
|
|
75
|
+
for (const [name, prop] of Object.entries(properties)) {
|
|
76
|
+
result[name] = generateExampleJson(prop)
|
|
77
|
+
}
|
|
78
|
+
return result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const defaults: Record<string, unknown> = {
|
|
82
|
+
string: 'string',
|
|
83
|
+
integer: 0,
|
|
84
|
+
number: 0,
|
|
85
|
+
boolean: true,
|
|
86
|
+
}
|
|
87
|
+
return defaults[schema.type as string] ?? null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function inferType(schema: OpenAPIV3.SchemaObject): string {
|
|
91
|
+
if (schema.type === 'array') {
|
|
92
|
+
const items = schema.items as OpenAPIV3.SchemaObject | undefined
|
|
93
|
+
const itemType = items ? inferType(items) : 'unknown'
|
|
94
|
+
return `${itemType}[]`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (schema.format) return `${schema.type}(${schema.format})`
|
|
98
|
+
return (schema.type as string) ?? 'object'
|
|
99
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
interface SnippetOptions {
|
|
2
|
+
method: string
|
|
3
|
+
url: string
|
|
4
|
+
headers: Record<string, string>
|
|
5
|
+
body?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function generateCurl({ method, url, headers, body }: SnippetOptions): string {
|
|
9
|
+
const parts = [`curl -X ${method} '${url}'`]
|
|
10
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
11
|
+
parts.push(` -H '${key}: ${value}'`)
|
|
12
|
+
}
|
|
13
|
+
if (body) {
|
|
14
|
+
parts.push(` -d '${body}'`)
|
|
15
|
+
}
|
|
16
|
+
return parts.join(' \\\n')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function generatePython({ method, url, headers, body }: SnippetOptions): string {
|
|
20
|
+
const lines: string[] = ['import requests', '']
|
|
21
|
+
const methodLower = method.toLowerCase()
|
|
22
|
+
const headerEntries = Object.entries(headers)
|
|
23
|
+
|
|
24
|
+
lines.push(`response = requests.${methodLower}(`)
|
|
25
|
+
lines.push(` "${url}",`)
|
|
26
|
+
|
|
27
|
+
if (headerEntries.length > 0) {
|
|
28
|
+
lines.push(' headers={')
|
|
29
|
+
for (const [key, value] of headerEntries) {
|
|
30
|
+
lines.push(` "${key}": "${value}",`)
|
|
31
|
+
}
|
|
32
|
+
lines.push(' },')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (body) {
|
|
36
|
+
lines.push(` json=${body},`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lines.push(')')
|
|
40
|
+
lines.push('print(response.json())')
|
|
41
|
+
return lines.join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function generateGo({ method, url, headers, body }: SnippetOptions): string {
|
|
45
|
+
const lines: string[] = []
|
|
46
|
+
|
|
47
|
+
if (body) {
|
|
48
|
+
lines.push('payload := strings.NewReader(`' + body + '`)')
|
|
49
|
+
lines.push('')
|
|
50
|
+
lines.push(`req, _ := http.NewRequest("${method}", "${url}", payload)`)
|
|
51
|
+
} else {
|
|
52
|
+
lines.push(`req, _ := http.NewRequest("${method}", "${url}", nil)`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
56
|
+
lines.push(`req.Header.Set("${key}", "${value}")`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
lines.push('')
|
|
60
|
+
lines.push('resp, _ := http.DefaultClient.Do(req)')
|
|
61
|
+
lines.push('defer resp.Body.Close()')
|
|
62
|
+
return lines.join('\n')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function generateTypeScript({ method, url, headers, body }: SnippetOptions): string {
|
|
66
|
+
const lines: string[] = []
|
|
67
|
+
const headerEntries = Object.entries(headers)
|
|
68
|
+
|
|
69
|
+
lines.push(`const response = await fetch("${url}", {`)
|
|
70
|
+
lines.push(` method: "${method}",`)
|
|
71
|
+
|
|
72
|
+
if (headerEntries.length > 0) {
|
|
73
|
+
lines.push(' headers: {')
|
|
74
|
+
for (const [key, value] of headerEntries) {
|
|
75
|
+
lines.push(` "${key}": "${value}",`)
|
|
76
|
+
}
|
|
77
|
+
lines.push(' },')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (body) {
|
|
81
|
+
lines.push(` body: JSON.stringify(${body}),`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push('});')
|
|
85
|
+
lines.push('const data = await response.json();')
|
|
86
|
+
return lines.join('\n')
|
|
87
|
+
}
|