@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.
Files changed (128) hide show
  1. package/bin/chronicle.js +2 -0
  2. package/dist/cli/index.js +963 -0
  3. package/package.json +67 -0
  4. package/src/cli/__tests__/config.test.ts +25 -0
  5. package/src/cli/__tests__/scaffold.test.ts +10 -0
  6. package/src/cli/commands/build.ts +74 -0
  7. package/src/cli/commands/dev.ts +21 -0
  8. package/src/cli/commands/init.ts +154 -0
  9. package/src/cli/commands/serve.ts +55 -0
  10. package/src/cli/commands/start.ts +24 -0
  11. package/src/cli/index.ts +21 -0
  12. package/src/cli/utils/config.ts +43 -0
  13. package/src/cli/utils/index.ts +2 -0
  14. package/src/cli/utils/resolve.ts +6 -0
  15. package/src/cli/utils/scaffold.ts +20 -0
  16. package/src/components/api/code-snippets.module.css +7 -0
  17. package/src/components/api/code-snippets.tsx +76 -0
  18. package/src/components/api/endpoint-page.module.css +58 -0
  19. package/src/components/api/endpoint-page.tsx +283 -0
  20. package/src/components/api/field-row.module.css +126 -0
  21. package/src/components/api/field-row.tsx +204 -0
  22. package/src/components/api/field-section.module.css +24 -0
  23. package/src/components/api/field-section.tsx +100 -0
  24. package/src/components/api/index.ts +8 -0
  25. package/src/components/api/json-editor.module.css +9 -0
  26. package/src/components/api/json-editor.tsx +61 -0
  27. package/src/components/api/key-value-editor.module.css +13 -0
  28. package/src/components/api/key-value-editor.tsx +62 -0
  29. package/src/components/api/method-badge.module.css +4 -0
  30. package/src/components/api/method-badge.tsx +29 -0
  31. package/src/components/api/response-panel.module.css +8 -0
  32. package/src/components/api/response-panel.tsx +44 -0
  33. package/src/components/common/breadcrumb.tsx +3 -0
  34. package/src/components/common/button.tsx +3 -0
  35. package/src/components/common/callout.module.css +7 -0
  36. package/src/components/common/callout.tsx +27 -0
  37. package/src/components/common/code-block.tsx +3 -0
  38. package/src/components/common/dialog.tsx +3 -0
  39. package/src/components/common/index.ts +10 -0
  40. package/src/components/common/input-field.tsx +3 -0
  41. package/src/components/common/sidebar.tsx +3 -0
  42. package/src/components/common/switch.tsx +3 -0
  43. package/src/components/common/table.tsx +3 -0
  44. package/src/components/common/tabs.tsx +3 -0
  45. package/src/components/mdx/code.module.css +42 -0
  46. package/src/components/mdx/code.tsx +36 -0
  47. package/src/components/mdx/details.module.css +14 -0
  48. package/src/components/mdx/details.tsx +17 -0
  49. package/src/components/mdx/image.tsx +24 -0
  50. package/src/components/mdx/index.tsx +35 -0
  51. package/src/components/mdx/link.tsx +37 -0
  52. package/src/components/mdx/mermaid.module.css +9 -0
  53. package/src/components/mdx/mermaid.tsx +37 -0
  54. package/src/components/mdx/paragraph.module.css +8 -0
  55. package/src/components/mdx/paragraph.tsx +19 -0
  56. package/src/components/mdx/table.tsx +40 -0
  57. package/src/components/ui/breadcrumbs.tsx +72 -0
  58. package/src/components/ui/client-theme-switcher.tsx +18 -0
  59. package/src/components/ui/footer.module.css +27 -0
  60. package/src/components/ui/footer.tsx +31 -0
  61. package/src/components/ui/search.module.css +111 -0
  62. package/src/components/ui/search.tsx +173 -0
  63. package/src/lib/api-routes.ts +120 -0
  64. package/src/lib/config.ts +56 -0
  65. package/src/lib/head.tsx +45 -0
  66. package/src/lib/index.ts +2 -0
  67. package/src/lib/openapi.ts +188 -0
  68. package/src/lib/page-context.tsx +95 -0
  69. package/src/lib/remark-unused-directives.ts +30 -0
  70. package/src/lib/schema.ts +99 -0
  71. package/src/lib/snippet-generators.ts +87 -0
  72. package/src/lib/source.ts +138 -0
  73. package/src/pages/ApiLayout.module.css +22 -0
  74. package/src/pages/ApiLayout.tsx +29 -0
  75. package/src/pages/ApiPage.tsx +68 -0
  76. package/src/pages/DocsLayout.tsx +18 -0
  77. package/src/pages/DocsPage.tsx +43 -0
  78. package/src/pages/NotFound.tsx +10 -0
  79. package/src/pages/__tests__/head.test.tsx +57 -0
  80. package/src/server/App.tsx +59 -0
  81. package/src/server/__tests__/entry-server.test.tsx +35 -0
  82. package/src/server/__tests__/handlers.test.ts +77 -0
  83. package/src/server/__tests__/og.test.ts +23 -0
  84. package/src/server/__tests__/router.test.ts +72 -0
  85. package/src/server/__tests__/vite-config.test.ts +25 -0
  86. package/src/server/adapters/vercel.ts +133 -0
  87. package/src/server/build-search-index.ts +107 -0
  88. package/src/server/dev.ts +156 -0
  89. package/src/server/entry-client.tsx +74 -0
  90. package/src/server/entry-prod.ts +97 -0
  91. package/src/server/entry-server.tsx +35 -0
  92. package/src/server/entry-vercel.ts +28 -0
  93. package/src/server/handlers/apis-proxy.ts +52 -0
  94. package/src/server/handlers/health.ts +3 -0
  95. package/src/server/handlers/llms.ts +58 -0
  96. package/src/server/handlers/og.ts +87 -0
  97. package/src/server/handlers/robots.ts +11 -0
  98. package/src/server/handlers/search.ts +172 -0
  99. package/src/server/handlers/sitemap.ts +39 -0
  100. package/src/server/handlers/specs.ts +9 -0
  101. package/src/server/index.html +12 -0
  102. package/src/server/prod.ts +18 -0
  103. package/src/server/request-handler.ts +63 -0
  104. package/src/server/router.ts +42 -0
  105. package/src/server/vite-config.ts +71 -0
  106. package/src/themes/default/Layout.module.css +81 -0
  107. package/src/themes/default/Layout.tsx +132 -0
  108. package/src/themes/default/Page.module.css +106 -0
  109. package/src/themes/default/Page.tsx +21 -0
  110. package/src/themes/default/Toc.module.css +48 -0
  111. package/src/themes/default/Toc.tsx +66 -0
  112. package/src/themes/default/font.ts +4 -0
  113. package/src/themes/default/index.ts +13 -0
  114. package/src/themes/paper/ChapterNav.module.css +71 -0
  115. package/src/themes/paper/ChapterNav.tsx +95 -0
  116. package/src/themes/paper/Layout.module.css +33 -0
  117. package/src/themes/paper/Layout.tsx +25 -0
  118. package/src/themes/paper/Page.module.css +174 -0
  119. package/src/themes/paper/Page.tsx +106 -0
  120. package/src/themes/paper/ReadingProgress.module.css +132 -0
  121. package/src/themes/paper/ReadingProgress.tsx +294 -0
  122. package/src/themes/paper/index.ts +8 -0
  123. package/src/themes/registry.ts +14 -0
  124. package/src/types/config.ts +80 -0
  125. package/src/types/content.ts +36 -0
  126. package/src/types/index.ts +3 -0
  127. package/src/types/theme.ts +22 -0
  128. 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
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './config'
2
+ export * from './source'
@@ -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
+ }