@axerity/cli 0.1.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/LICENSE +21 -0
- package/README.md +1 -0
- package/axerity.default.json +135 -0
- package/axerity.schema.json +268 -0
- package/bin/axerity.js +305 -0
- package/mdsvex.config.js +261 -0
- package/package.json +103 -0
- package/scripts/prepare-engine.mjs +20 -0
- package/src/app.d.ts +17 -0
- package/src/app.html +39 -0
- package/src/content/demo/api/meta.json +5 -0
- package/src/content/demo/api/pet/add-pet.md +105 -0
- package/src/content/demo/api/pet/delete-pet.md +70 -0
- package/src/content/demo/api/pet/find-by-status.md +72 -0
- package/src/content/demo/api/pet/find-by-tags.md +64 -0
- package/src/content/demo/api/pet/get-pet.md +99 -0
- package/src/content/demo/api/pet/meta.json +15 -0
- package/src/content/demo/api/pet/pet-object.md +112 -0
- package/src/content/demo/api/pet/update-pet-with-form.md +69 -0
- package/src/content/demo/api/pet/update-pet.md +79 -0
- package/src/content/demo/api/pet/upload-image.md +79 -0
- package/src/content/demo/api/store/delete-order.md +62 -0
- package/src/content/demo/api/store/get-order.md +70 -0
- package/src/content/demo/api/store/inventory.md +54 -0
- package/src/content/demo/api/store/meta.json +5 -0
- package/src/content/demo/api/store/order-object.md +77 -0
- package/src/content/demo/api/store/place-order.md +83 -0
- package/src/content/demo/api/user/create-user.md +69 -0
- package/src/content/demo/api/user/create-with-list.md +57 -0
- package/src/content/demo/api/user/delete-user.md +61 -0
- package/src/content/demo/api/user/get-user.md +69 -0
- package/src/content/demo/api/user/login.md +80 -0
- package/src/content/demo/api/user/logout.md +45 -0
- package/src/content/demo/api/user/meta.json +14 -0
- package/src/content/demo/api/user/update-user.md +69 -0
- package/src/content/demo/api/user/user-object.md +85 -0
- package/src/content/demo/changelog.md +44 -0
- package/src/content/demo/components/accordion.md +70 -0
- package/src/content/demo/components/api.md +185 -0
- package/src/content/demo/components/badge.md +34 -0
- package/src/content/demo/components/callout.md +83 -0
- package/src/content/demo/components/cards.md +88 -0
- package/src/content/demo/components/code-group.md +55 -0
- package/src/content/demo/components/columns.md +42 -0
- package/src/content/demo/components/frame.md +51 -0
- package/src/content/demo/components/icon.md +54 -0
- package/src/content/demo/components/kbd.md +28 -0
- package/src/content/demo/components/meta.json +26 -0
- package/src/content/demo/components/roadmap.md +86 -0
- package/src/content/demo/components/steps.md +72 -0
- package/src/content/demo/components/tabs.md +146 -0
- package/src/content/demo/components/tooltip.md +44 -0
- package/src/content/demo/components/tree.md +83 -0
- package/src/content/demo/components/type-table.md +77 -0
- package/src/content/demo/components/update.md +48 -0
- package/src/content/demo/components/video.md +56 -0
- package/src/content/demo/components/webhooks.md +109 -0
- package/src/content/demo/components/websockets.md +101 -0
- package/src/content/demo/configuration/ai.md +40 -0
- package/src/content/demo/configuration/cli.md +46 -0
- package/src/content/demo/configuration/deployment.md +105 -0
- package/src/content/demo/configuration/index.md +92 -0
- package/src/content/demo/configuration/layouts.md +51 -0
- package/src/content/demo/configuration/meta.json +5 -0
- package/src/content/demo/configuration/navigation.md +167 -0
- package/src/content/demo/configuration/openapi.md +103 -0
- package/src/content/demo/configuration/search.md +36 -0
- package/src/content/demo/index.md +59 -0
- package/src/content/demo/installation.md +49 -0
- package/src/content/demo/meta.json +15 -0
- package/src/content/demo/quick-start.md +47 -0
- package/src/content/demo/theming/advanced.md +116 -0
- package/src/content/demo/theming/code.md +66 -0
- package/src/content/demo/theming/colors.md +103 -0
- package/src/content/demo/theming/index.md +88 -0
- package/src/content/demo/theming/layout.md +71 -0
- package/src/content/demo/theming/meta.json +5 -0
- package/src/content/demo/theming/themes.md +99 -0
- package/src/content/demo/theming/typography.md +83 -0
- package/src/content/demo/writing/code-blocks.md +154 -0
- package/src/content/demo/writing/diagrams.md +44 -0
- package/src/content/demo/writing/frontmatter.md +33 -0
- package/src/content/demo/writing/markdown.md +62 -0
- package/src/content/demo/writing/meta.json +5 -0
- package/src/hooks.server.ts +49 -0
- package/src/lib/assets/favicon.svg +1 -0
- package/src/lib/base.ts +12 -0
- package/src/lib/components/DynamicIcon.svelte +26 -0
- package/src/lib/components/docs/Analytics.svelte +38 -0
- package/src/lib/components/docs/Banner.svelte +44 -0
- package/src/lib/components/docs/Breadcrumbs.svelte +38 -0
- package/src/lib/components/docs/CopyPageMenu.svelte +119 -0
- package/src/lib/components/docs/DocsLayout.svelte +192 -0
- package/src/lib/components/docs/Footer.svelte +60 -0
- package/src/lib/components/docs/Mermaid.svelte +39 -0
- package/src/lib/components/docs/Navbar.svelte +144 -0
- package/src/lib/components/docs/PageMeta.svelte +35 -0
- package/src/lib/components/docs/PageNav.svelte +44 -0
- package/src/lib/components/docs/SearchDialog.svelte +182 -0
- package/src/lib/components/docs/Sidebar.svelte +85 -0
- package/src/lib/components/docs/SidebarDropdown.svelte +56 -0
- package/src/lib/components/docs/SidebarFooterLinks.svelte +19 -0
- package/src/lib/components/docs/SidebarGroup.svelte +54 -0
- package/src/lib/components/docs/SidebarLink.svelte +67 -0
- package/src/lib/components/docs/TableOfContents.svelte +77 -0
- package/src/lib/components/docs/ThemeToggle.svelte +19 -0
- package/src/lib/components/docs/VersionSwitcher.svelte +80 -0
- package/src/lib/components/kit/Accordion.svelte +60 -0
- package/src/lib/components/kit/AccordionGroup.svelte +13 -0
- package/src/lib/components/kit/Badge.svelte +32 -0
- package/src/lib/components/kit/Callout.svelte +51 -0
- package/src/lib/components/kit/Card.svelte +72 -0
- package/src/lib/components/kit/CardGroup.svelte +21 -0
- package/src/lib/components/kit/CodeGroup.svelte +65 -0
- package/src/lib/components/kit/Columns.svelte +26 -0
- package/src/lib/components/kit/Event.svelte +23 -0
- package/src/lib/components/kit/EventList.svelte +9 -0
- package/src/lib/components/kit/File.svelte +15 -0
- package/src/lib/components/kit/Folder.svelte +46 -0
- package/src/lib/components/kit/Frame.svelte +81 -0
- package/src/lib/components/kit/Icon.svelte +17 -0
- package/src/lib/components/kit/Kbd.svelte +11 -0
- package/src/lib/components/kit/Roadmap.svelte +15 -0
- package/src/lib/components/kit/RoadmapItem.svelte +109 -0
- package/src/lib/components/kit/Step.svelte +63 -0
- package/src/lib/components/kit/Steps.svelte +16 -0
- package/src/lib/components/kit/Tab.svelte +27 -0
- package/src/lib/components/kit/Tabs.svelte +75 -0
- package/src/lib/components/kit/Tooltip.svelte +33 -0
- package/src/lib/components/kit/Tree.svelte +11 -0
- package/src/lib/components/kit/TypeTable.svelte +187 -0
- package/src/lib/components/kit/Update.svelte +32 -0
- package/src/lib/components/kit/Video.svelte +64 -0
- package/src/lib/components/kit/accordion-context.ts +1 -0
- package/src/lib/components/kit/api/Api.svelte +80 -0
- package/src/lib/components/kit/api/ApiExamplePanel.svelte +100 -0
- package/src/lib/components/kit/api/ApiField.svelte +124 -0
- package/src/lib/components/kit/api/Channel.svelte +121 -0
- package/src/lib/components/kit/api/Endpoint.svelte +116 -0
- package/src/lib/components/kit/api/Enum.svelte +44 -0
- package/src/lib/components/kit/api/EnumValues.svelte +35 -0
- package/src/lib/components/kit/api/Expandable.svelte +70 -0
- package/src/lib/components/kit/api/Message.svelte +67 -0
- package/src/lib/components/kit/api/ObjectExample.svelte +11 -0
- package/src/lib/components/kit/api/RequestExample.svelte +11 -0
- package/src/lib/components/kit/api/ResponseExample.svelte +11 -0
- package/src/lib/components/kit/api/Webhook.svelte +115 -0
- package/src/lib/components/kit/api/api-context.ts +15 -0
- package/src/lib/components/kit/tabs-context.ts +8 -0
- package/src/lib/components/kit/tabs-store.svelte.ts +28 -0
- package/src/lib/config/site.ts +34 -0
- package/src/lib/content/index.ts +50 -0
- package/src/lib/content/raw.ts +21 -0
- package/src/lib/content/tree.ts +169 -0
- package/src/lib/index.ts +79 -0
- package/src/lib/nav-match.ts +23 -0
- package/src/lib/openapi/generate.ts +629 -0
- package/src/lib/server/og.ts +140 -0
- package/src/lib/state/search.svelte.ts +9 -0
- package/src/lib/state/theme.svelte.ts +58 -0
- package/src/lib/types.ts +216 -0
- package/src/params/docpage.ts +3 -0
- package/src/params/mdfile.ts +3 -0
- package/src/routes/+error.svelte +46 -0
- package/src/routes/+layout.svelte +25 -0
- package/src/routes/[...path=mdfile]/+server.ts +21 -0
- package/src/routes/[...slug=docpage]/+page.svelte +63 -0
- package/src/routes/[...slug=docpage]/+page.ts +44 -0
- package/src/routes/layout.css +897 -0
- package/src/routes/llms-full.txt/+server.ts +22 -0
- package/src/routes/llms.txt/+server.ts +20 -0
- package/src/routes/og/[...slug]/+server.ts +77 -0
- package/src/routes/rss.xml/+server.ts +65 -0
- package/src/routes/search.json/+server.ts +54 -0
- package/src/routes/sitemap.xml/+server.ts +21 -0
- package/static/favicon-dark.svg +6 -0
- package/static/favicon-light.svg +6 -0
- package/static/favicon.svg +14 -0
- package/static/fonts/geist-400.ttf +0 -0
- package/static/fonts/geist-600.ttf +0 -0
- package/static/fonts/geist-700.ttf +0 -0
- package/static/og-image.png +0 -0
- package/static/robots.txt +4 -0
- package/svelte.config.js +35 -0
- package/tsconfig.json +20 -0
- package/vite.config.ts +46 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
|
|
5
|
+
interface ApiSource {
|
|
6
|
+
spec: string;
|
|
7
|
+
output?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Schema {
|
|
12
|
+
$ref?: string;
|
|
13
|
+
type?: string | string[];
|
|
14
|
+
format?: string;
|
|
15
|
+
enum?: unknown[];
|
|
16
|
+
default?: unknown;
|
|
17
|
+
example?: unknown;
|
|
18
|
+
description?: string;
|
|
19
|
+
deprecated?: boolean;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
writeOnly?: boolean;
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
required?: string[];
|
|
24
|
+
items?: Schema;
|
|
25
|
+
properties?: Record<string, Schema>;
|
|
26
|
+
allOf?: Schema[];
|
|
27
|
+
oneOf?: Schema[];
|
|
28
|
+
anyOf?: Schema[];
|
|
29
|
+
minLength?: number;
|
|
30
|
+
maxLength?: number;
|
|
31
|
+
minimum?: number;
|
|
32
|
+
maximum?: number;
|
|
33
|
+
pattern?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface MediaType {
|
|
37
|
+
schema?: Schema;
|
|
38
|
+
example?: unknown;
|
|
39
|
+
examples?: Record<string, { value?: unknown }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Parameter {
|
|
43
|
+
$ref?: string;
|
|
44
|
+
name: string;
|
|
45
|
+
in: string;
|
|
46
|
+
required?: boolean;
|
|
47
|
+
deprecated?: boolean;
|
|
48
|
+
description?: string;
|
|
49
|
+
schema?: Schema;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Header {
|
|
53
|
+
schema?: Schema;
|
|
54
|
+
description?: string;
|
|
55
|
+
required?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface Body {
|
|
59
|
+
$ref?: string;
|
|
60
|
+
description?: string;
|
|
61
|
+
content?: Record<string, MediaType>;
|
|
62
|
+
headers?: Record<string, Header>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
type SecurityRequirement = Record<string, string[]>;
|
|
66
|
+
|
|
67
|
+
interface Operation {
|
|
68
|
+
tags?: string[];
|
|
69
|
+
summary?: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
operationId?: string;
|
|
72
|
+
deprecated?: boolean;
|
|
73
|
+
parameters?: Parameter[];
|
|
74
|
+
requestBody?: Body;
|
|
75
|
+
responses?: Record<string, Body>;
|
|
76
|
+
security?: SecurityRequirement[];
|
|
77
|
+
servers?: { url: string }[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface PathItem {
|
|
81
|
+
servers?: { url: string }[];
|
|
82
|
+
parameters?: Parameter[];
|
|
83
|
+
get?: Operation;
|
|
84
|
+
post?: Operation;
|
|
85
|
+
put?: Operation;
|
|
86
|
+
patch?: Operation;
|
|
87
|
+
delete?: Operation;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface SecurityScheme {
|
|
91
|
+
type?: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
in?: string;
|
|
94
|
+
scheme?: string;
|
|
95
|
+
bearerFormat?: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
flows?: Record<string, { scopes?: Record<string, string> }>;
|
|
98
|
+
openIdConnectUrl?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface Spec {
|
|
102
|
+
info?: { title?: string };
|
|
103
|
+
servers?: { url: string }[];
|
|
104
|
+
tags?: { name: string }[];
|
|
105
|
+
security?: SecurityRequirement[];
|
|
106
|
+
paths?: Record<string, PathItem>;
|
|
107
|
+
webhooks?: Record<string, PathItem>;
|
|
108
|
+
components?: {
|
|
109
|
+
schemas?: Record<string, Schema>;
|
|
110
|
+
securitySchemes?: Record<string, SecurityScheme>;
|
|
111
|
+
};
|
|
112
|
+
[key: string]: unknown;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const METHODS = ['get', 'post', 'put', 'patch', 'delete'] as const;
|
|
116
|
+
const METHOD_ICON: Record<string, string> = {
|
|
117
|
+
get: 'arrow-down-to-line',
|
|
118
|
+
post: 'plus',
|
|
119
|
+
put: 'pencil',
|
|
120
|
+
patch: 'pencil',
|
|
121
|
+
delete: 'trash-2'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
125
|
+
typeof value === 'object' && value !== null;
|
|
126
|
+
|
|
127
|
+
// Escape characters Svelte would otherwise read as markup/expressions in the
|
|
128
|
+
// Markdown body (outside code blocks, which mdsvex already escapes).
|
|
129
|
+
const escapeText = (text: string): string =>
|
|
130
|
+
text.replace(/[<{}]/g, (c) => ({ '<': '<', '{': '{', '}': '}' })[c] ?? c);
|
|
131
|
+
|
|
132
|
+
function slugify(value: string): string {
|
|
133
|
+
return value
|
|
134
|
+
.replace(/\{[^}]+\}/g, (m) => m.slice(1, -1))
|
|
135
|
+
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
136
|
+
.replace(/^-+|-+$/g, '')
|
|
137
|
+
.toLowerCase();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const refName = (ref: string): string => ref.slice(ref.lastIndexOf('/') + 1);
|
|
141
|
+
|
|
142
|
+
function resolve<T>(spec: Spec, node: T): T {
|
|
143
|
+
let current: unknown = node;
|
|
144
|
+
const seen = new Set<string>();
|
|
145
|
+
while (isObject(current) && typeof current.$ref === 'string' && !seen.has(current.$ref)) {
|
|
146
|
+
seen.add(current.$ref);
|
|
147
|
+
const parts = current.$ref.replace(/^#\//, '').split('/');
|
|
148
|
+
current = parts.reduce<unknown>((acc, key) => (isObject(acc) ? acc[key] : undefined), spec);
|
|
149
|
+
}
|
|
150
|
+
return (current ?? {}) as T;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Merge `allOf` into a single object schema; keep oneOf/anyOf for labelling.
|
|
154
|
+
function composed(spec: Spec, schema: Schema): Schema {
|
|
155
|
+
const s = resolve(spec, schema);
|
|
156
|
+
if (!s.allOf) return s;
|
|
157
|
+
const merged: Schema = { type: 'object', properties: {}, required: [] };
|
|
158
|
+
for (const part of [...s.allOf, s]) {
|
|
159
|
+
const r = resolve(spec, part);
|
|
160
|
+
Object.assign(merged.properties!, r.properties);
|
|
161
|
+
if (r.required) merged.required!.push(...r.required);
|
|
162
|
+
}
|
|
163
|
+
return merged;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const baseType = (type?: string | string[]): string | undefined =>
|
|
167
|
+
Array.isArray(type) ? type.find((t) => t !== 'null') : type;
|
|
168
|
+
|
|
169
|
+
function typeLabel(spec: Spec, schema?: Schema): string {
|
|
170
|
+
if (!schema) return 'any';
|
|
171
|
+
if (schema.$ref) return refName(schema.$ref);
|
|
172
|
+
if (schema.oneOf || schema.anyOf) {
|
|
173
|
+
const variants = (schema.oneOf ?? schema.anyOf)!.map((v) => typeLabel(spec, v));
|
|
174
|
+
return [...new Set(variants)].join(' or ');
|
|
175
|
+
}
|
|
176
|
+
if (schema.enum) return 'enum';
|
|
177
|
+
const type = baseType(schema.type);
|
|
178
|
+
if (type === 'array') {
|
|
179
|
+
const items = schema.items?.$ref ? refName(schema.items.$ref) : typeLabel(spec, schema.items);
|
|
180
|
+
return `array of ${items}`;
|
|
181
|
+
}
|
|
182
|
+
return schema.format ?? type ?? 'object';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function describe(schema: Schema): string {
|
|
186
|
+
const lines: string[] = [];
|
|
187
|
+
if (schema.description) lines.push(schema.description);
|
|
188
|
+
const notes: string[] = [];
|
|
189
|
+
if (schema.enum) notes.push(`one of ${schema.enum.map((v) => `\`${v}\``).join(', ')}`);
|
|
190
|
+
if (schema.format && !schema.description?.includes(schema.format))
|
|
191
|
+
notes.push(`format \`${schema.format}\``);
|
|
192
|
+
if (schema.pattern) notes.push(`pattern \`${schema.pattern}\``);
|
|
193
|
+
if (schema.minLength !== undefined || schema.maxLength !== undefined) {
|
|
194
|
+
notes.push(`length ${schema.minLength ?? 0}–${schema.maxLength ?? '∞'}`);
|
|
195
|
+
}
|
|
196
|
+
if (schema.minimum !== undefined || schema.maximum !== undefined) {
|
|
197
|
+
notes.push(`range ${schema.minimum ?? '−∞'}–${schema.maximum ?? '∞'}`);
|
|
198
|
+
}
|
|
199
|
+
if (schema.default !== undefined && typeof schema.default !== 'object') {
|
|
200
|
+
notes.push(`defaults to \`${schema.default}\``);
|
|
201
|
+
}
|
|
202
|
+
if (schema.readOnly) notes.push('read-only');
|
|
203
|
+
if (schema.writeOnly) notes.push('write-only');
|
|
204
|
+
if (schema.deprecated) notes.push('deprecated');
|
|
205
|
+
if (notes.length) lines.push(`(${notes.join(', ')})`);
|
|
206
|
+
return lines.join(' ');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function attrs(spec: Spec, name: string, schema: Schema, required?: boolean): string {
|
|
210
|
+
const parts = [`name="${name}"`, `type="${typeLabel(spec, schema)}"`];
|
|
211
|
+
if (required) parts.push('required');
|
|
212
|
+
if (schema.deprecated) parts.push('deprecated');
|
|
213
|
+
if (schema.default !== undefined && typeof schema.default !== 'object') {
|
|
214
|
+
parts.push(`default="${String(schema.default).replace(/"/g, '"')}"`);
|
|
215
|
+
}
|
|
216
|
+
return parts.join(' ');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Render a property list. Inline nested objects/arrays expand inline; $ref types
|
|
220
|
+
// link to their generated schema page.
|
|
221
|
+
function fields(spec: Spec, schema: Schema, group: string, component: string, depth = 0): string {
|
|
222
|
+
const resolved = composed(spec, schema);
|
|
223
|
+
const props = resolved.properties ?? {};
|
|
224
|
+
const required = new Set(resolved.required ?? []);
|
|
225
|
+
const out: string[] = [];
|
|
226
|
+
for (const [name, raw] of Object.entries(props)) {
|
|
227
|
+
const isRef = typeof raw.$ref === 'string';
|
|
228
|
+
const prop = composed(spec, raw);
|
|
229
|
+
const link = isRef ? ` typeLink="/${group}/schemas/${slugify(refName(raw.$ref!))}"` : '';
|
|
230
|
+
const labelSchema = isRef ? ({ $ref: raw.$ref } as Schema) : prop;
|
|
231
|
+
out.push(`<${component} ${attrs(spec, name, labelSchema, required.has(name))}${link}>`, '');
|
|
232
|
+
out.push(escapeText(describe(prop)) || 'No description.', '');
|
|
233
|
+
|
|
234
|
+
const inlineObject = !isRef && (baseType(prop.type) === 'object' || prop.properties);
|
|
235
|
+
const arrayItem =
|
|
236
|
+
!isRef && baseType(prop.type) === 'array' ? composed(spec, prop.items ?? {}) : null;
|
|
237
|
+
if (inlineObject && depth < 3) {
|
|
238
|
+
out.push(
|
|
239
|
+
'<Expandable title="properties">',
|
|
240
|
+
'',
|
|
241
|
+
fields(spec, prop, group, component, depth + 1),
|
|
242
|
+
'</Expandable>',
|
|
243
|
+
''
|
|
244
|
+
);
|
|
245
|
+
} else if (arrayItem && arrayItem.properties && !prop.items?.$ref && depth < 3) {
|
|
246
|
+
out.push(
|
|
247
|
+
'<Expandable title="items">',
|
|
248
|
+
'',
|
|
249
|
+
fields(spec, arrayItem, group, component, depth + 1),
|
|
250
|
+
'</Expandable>',
|
|
251
|
+
''
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
out.push(`</${component}>`, '');
|
|
255
|
+
}
|
|
256
|
+
return out.join('\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function sample(spec: Spec, schema: Schema | undefined, depth = 0): unknown {
|
|
260
|
+
const s = composed(spec, schema ?? {});
|
|
261
|
+
if (depth > 5) return null;
|
|
262
|
+
if (s.example !== undefined) return s.example;
|
|
263
|
+
if (s.default !== undefined) return s.default;
|
|
264
|
+
if (s.enum) return s.enum[0];
|
|
265
|
+
if (s.oneOf || s.anyOf) return sample(spec, (s.oneOf ?? s.anyOf)![0], depth + 1);
|
|
266
|
+
const type = baseType(s.type);
|
|
267
|
+
if (type === 'array') return [sample(spec, s.items, depth + 1)].filter((v) => v !== null);
|
|
268
|
+
if (type === 'object' || s.properties) {
|
|
269
|
+
const obj: Record<string, unknown> = {};
|
|
270
|
+
for (const [name, prop] of Object.entries(s.properties ?? {}))
|
|
271
|
+
obj[name] = sample(spec, prop, depth + 1);
|
|
272
|
+
return obj;
|
|
273
|
+
}
|
|
274
|
+
if (type === 'integer' || type === 'number') return 0;
|
|
275
|
+
if (type === 'boolean') return true;
|
|
276
|
+
if (s.format === 'date-time') return '2024-01-01T00:00:00Z';
|
|
277
|
+
return type === 'string' ? 'string' : null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function pickMedia(spec: Spec, body?: Body): MediaType | undefined {
|
|
281
|
+
const content = resolve(spec, body ?? {}).content;
|
|
282
|
+
if (!content) return undefined;
|
|
283
|
+
return content['application/json'] ?? content[Object.keys(content)[0]];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function bodyExample(spec: Spec, media?: MediaType): unknown {
|
|
287
|
+
if (!media) return undefined;
|
|
288
|
+
if (media.example !== undefined) return media.example;
|
|
289
|
+
const named = media.examples && Object.values(media.examples)[0];
|
|
290
|
+
if (named && named.value !== undefined) return named.value;
|
|
291
|
+
return media.schema ? sample(spec, media.schema) : undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const yamlLine = (text: string): string => JSON.stringify(text.split('\n')[0]);
|
|
295
|
+
|
|
296
|
+
const SCRIPT_IMPORT =
|
|
297
|
+
"\timport { Api, Endpoint, ParamField, ResponseField, RequestExample, ResponseExample, ObjectExample, Expandable } from '$lib';";
|
|
298
|
+
|
|
299
|
+
interface Page {
|
|
300
|
+
tag: string;
|
|
301
|
+
slug: string;
|
|
302
|
+
body: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function frontmatter(title: string, description: string, method: string, icon: string): string[] {
|
|
306
|
+
return [
|
|
307
|
+
'---',
|
|
308
|
+
`title: ${yamlLine(title)}`,
|
|
309
|
+
...(description ? [`description: ${yamlLine(description)}`] : []),
|
|
310
|
+
'layout: api',
|
|
311
|
+
...(method ? [`method: ${method}`] : []),
|
|
312
|
+
`icon: ${icon}`,
|
|
313
|
+
'---',
|
|
314
|
+
'',
|
|
315
|
+
'<script>',
|
|
316
|
+
SCRIPT_IMPORT,
|
|
317
|
+
'</script>',
|
|
318
|
+
''
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function describeScheme(name: string, scheme: SecurityScheme, scopes: string[]): string {
|
|
323
|
+
if (scheme.type === 'http' && scheme.scheme === 'bearer') {
|
|
324
|
+
return `a bearer token${scheme.bearerFormat ? ` (${scheme.bearerFormat})` : ''}`;
|
|
325
|
+
}
|
|
326
|
+
if (scheme.type === 'http' && scheme.scheme === 'basic') return 'HTTP basic auth';
|
|
327
|
+
if (scheme.type === 'apiKey') return `an API key in the ${scheme.in} \`${scheme.name}\``;
|
|
328
|
+
if (scheme.type === 'oauth2') {
|
|
329
|
+
const flowScopes = Object.keys(Object.values(scheme.flows ?? {})[0]?.scopes ?? {});
|
|
330
|
+
const all = scopes.length ? scopes : flowScopes;
|
|
331
|
+
return `OAuth2${all.length ? ` (scopes ${all.map((s) => `\`${s}\``).join(', ')})` : ''}`;
|
|
332
|
+
}
|
|
333
|
+
if (scheme.type === 'openIdConnect') return 'OpenID Connect';
|
|
334
|
+
return name;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function authLines(spec: Spec, op: Operation): string[] {
|
|
338
|
+
const reqs = op.security ?? spec.security;
|
|
339
|
+
if (!reqs?.length) return [];
|
|
340
|
+
const schemes = spec.components?.securitySchemes ?? {};
|
|
341
|
+
const alternatives = reqs
|
|
342
|
+
.filter((r) => Object.keys(r).length)
|
|
343
|
+
.map((r) =>
|
|
344
|
+
Object.entries(r)
|
|
345
|
+
.map(([n, scopes]) => (schemes[n] ? describeScheme(n, schemes[n], scopes) : n))
|
|
346
|
+
.join(' and ')
|
|
347
|
+
);
|
|
348
|
+
if (!alternatives.length) return [];
|
|
349
|
+
const optional = reqs.some((r) => Object.keys(r).length === 0);
|
|
350
|
+
const text = `Requires ${alternatives.join(' or ')}.${optional ? ' Authentication is optional.' : ''}`;
|
|
351
|
+
return ['## Authentication', '', escapeText(text), ''];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function operationPage(
|
|
355
|
+
spec: Spec,
|
|
356
|
+
specBaseUrl: string,
|
|
357
|
+
group: string,
|
|
358
|
+
path: string,
|
|
359
|
+
method: string,
|
|
360
|
+
op: Operation,
|
|
361
|
+
pathItem?: PathItem,
|
|
362
|
+
defaultTag = 'default'
|
|
363
|
+
): Page {
|
|
364
|
+
const tag = slugify(op.tags?.[0] ?? defaultTag);
|
|
365
|
+
const title = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
|
|
366
|
+
const description = op.description ?? op.summary ?? '';
|
|
367
|
+
const pathExpr = path.includes('{') ? `path={'${path}'}` : `path="${path}"`;
|
|
368
|
+
const baseUrl = op.servers?.[0]?.url ?? pathItem?.servers?.[0]?.url ?? specBaseUrl;
|
|
369
|
+
|
|
370
|
+
const lines: string[] = [
|
|
371
|
+
...frontmatter(title, description, method.toUpperCase(), METHOD_ICON[method] ?? 'code'),
|
|
372
|
+
`# ${escapeText(title)}`,
|
|
373
|
+
'',
|
|
374
|
+
...(op.deprecated ? ['> This operation is deprecated.', ''] : []),
|
|
375
|
+
...(description ? [escapeText(description), ''] : []),
|
|
376
|
+
'<Api>',
|
|
377
|
+
'',
|
|
378
|
+
`<Endpoint method="${method.toUpperCase()}" ${pathExpr} baseUrl="${baseUrl}" />`,
|
|
379
|
+
''
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
lines.push(...authLines(spec, op));
|
|
383
|
+
|
|
384
|
+
// Path-level params are shared across operations; the operation's own win.
|
|
385
|
+
const paramMap = new Map<string, Parameter>();
|
|
386
|
+
for (const p of [...(pathItem?.parameters ?? []), ...(op.parameters ?? [])].map((x) =>
|
|
387
|
+
resolve(spec, x)
|
|
388
|
+
)) {
|
|
389
|
+
paramMap.set(`${p.in}:${p.name}`, p);
|
|
390
|
+
}
|
|
391
|
+
const params = [...paramMap.values()];
|
|
392
|
+
const groups: Record<string, string> = {
|
|
393
|
+
path: 'Path parameters',
|
|
394
|
+
query: 'Query parameters',
|
|
395
|
+
header: 'Header parameters',
|
|
396
|
+
cookie: 'Cookie parameters'
|
|
397
|
+
};
|
|
398
|
+
for (const where of Object.keys(groups)) {
|
|
399
|
+
const list = params.filter((p) => p.in === where);
|
|
400
|
+
if (!list.length) continue;
|
|
401
|
+
lines.push(`## ${groups[where]}`, '');
|
|
402
|
+
for (const p of list) {
|
|
403
|
+
lines.push(`<ParamField ${attrs(spec, p.name, p.schema ?? {}, p.required)}>`, '');
|
|
404
|
+
lines.push(
|
|
405
|
+
escapeText(describe(p.schema ?? {}) || p.description || '') || 'No description.',
|
|
406
|
+
''
|
|
407
|
+
);
|
|
408
|
+
lines.push('</ParamField>', '');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const reqContent = resolve(spec, op.requestBody ?? {}).content ?? {};
|
|
413
|
+
const reqTypes = Object.keys(reqContent);
|
|
414
|
+
const reqKey = reqContent['application/json'] ? 'application/json' : reqTypes[0];
|
|
415
|
+
const reqMedia = reqKey ? reqContent[reqKey] : undefined;
|
|
416
|
+
if (reqMedia?.schema) {
|
|
417
|
+
lines.push('## Body parameters', '');
|
|
418
|
+
if (reqTypes.length > 1) {
|
|
419
|
+
lines.push(`Accepts ${reqTypes.map((t) => `\`${t}\``).join(', ')}.`, '');
|
|
420
|
+
}
|
|
421
|
+
lines.push(fields(spec, reqMedia.schema, group, 'ParamField'));
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const reqExample = bodyExample(spec, reqMedia);
|
|
425
|
+
const curl = [
|
|
426
|
+
`curl -X ${method.toUpperCase()} ${baseUrl}${path.replace(/\{([^}]+)\}/g, ':$1')}`,
|
|
427
|
+
reqExample !== undefined
|
|
428
|
+
? ` -H "Content-Type: ${reqKey}" \\\n -d '${JSON.stringify(reqExample)}'`
|
|
429
|
+
: ''
|
|
430
|
+
]
|
|
431
|
+
.filter(Boolean)
|
|
432
|
+
.join(' \\\n');
|
|
433
|
+
lines.push(
|
|
434
|
+
'<RequestExample title="cURL">',
|
|
435
|
+
'',
|
|
436
|
+
'```bash',
|
|
437
|
+
curl,
|
|
438
|
+
'```',
|
|
439
|
+
'',
|
|
440
|
+
'</RequestExample>',
|
|
441
|
+
''
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const responses = Object.entries(op.responses ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
445
|
+
for (const [code, raw] of responses) {
|
|
446
|
+
const response = resolve(spec, raw);
|
|
447
|
+
const media = pickMedia(spec, response);
|
|
448
|
+
lines.push(`## Response \`${code}\``, '');
|
|
449
|
+
if (response.description) lines.push(escapeText(response.description), '');
|
|
450
|
+
if (response.headers) {
|
|
451
|
+
lines.push('Headers:', '');
|
|
452
|
+
for (const [hname, hraw] of Object.entries(response.headers)) {
|
|
453
|
+
const h = resolve(spec, hraw);
|
|
454
|
+
const desc = h.description ? ` — ${escapeText(h.description)}` : '';
|
|
455
|
+
lines.push(`- \`${hname}\` ${typeLabel(spec, h.schema)}${desc}`);
|
|
456
|
+
}
|
|
457
|
+
lines.push('');
|
|
458
|
+
}
|
|
459
|
+
if (media?.schema) lines.push(fields(spec, media.schema, group, 'ResponseField'));
|
|
460
|
+
const example = bodyExample(spec, media);
|
|
461
|
+
if (example !== undefined) {
|
|
462
|
+
lines.push(
|
|
463
|
+
`<ResponseExample title="${code}">`,
|
|
464
|
+
'',
|
|
465
|
+
'```json',
|
|
466
|
+
JSON.stringify(example, null, 2),
|
|
467
|
+
'```',
|
|
468
|
+
'',
|
|
469
|
+
'</ResponseExample>',
|
|
470
|
+
''
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
lines.push('</Api>', '');
|
|
476
|
+
return { tag, slug: slugify(op.operationId ?? `${method}-${path}`), body: lines.join('\n') };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function schemaPage(spec: Spec, group: string, name: string, schema: Schema): string {
|
|
480
|
+
const resolved = composed(spec, schema);
|
|
481
|
+
return [
|
|
482
|
+
...frontmatter(name, resolved.description ?? `The ${name} object.`, '', 'box'),
|
|
483
|
+
`# ${escapeText(name)}`,
|
|
484
|
+
'',
|
|
485
|
+
...(resolved.description ? [escapeText(resolved.description), ''] : []),
|
|
486
|
+
'<Api>',
|
|
487
|
+
'',
|
|
488
|
+
'## Attributes',
|
|
489
|
+
'',
|
|
490
|
+
fields(spec, resolved, group, 'ResponseField'),
|
|
491
|
+
'<ObjectExample title={`' + name + '`}>',
|
|
492
|
+
'',
|
|
493
|
+
'```json',
|
|
494
|
+
JSON.stringify(sample(spec, resolved), null, 2),
|
|
495
|
+
'```',
|
|
496
|
+
'',
|
|
497
|
+
'</ObjectExample>',
|
|
498
|
+
'',
|
|
499
|
+
'</Api>',
|
|
500
|
+
''
|
|
501
|
+
].join('\n');
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const titleCase = (value: string): string => value.replace(/(^|\s)\S/g, (c) => c.toUpperCase());
|
|
505
|
+
|
|
506
|
+
async function loadSpec(spec: string): Promise<Spec> {
|
|
507
|
+
let text: string;
|
|
508
|
+
if (/^https?:\/\//.test(spec)) {
|
|
509
|
+
const res = await fetch(spec);
|
|
510
|
+
text = await res.text();
|
|
511
|
+
} else {
|
|
512
|
+
text = readFileSync(spec, 'utf8');
|
|
513
|
+
}
|
|
514
|
+
text = text.trim();
|
|
515
|
+
return (text.startsWith('{') ? JSON.parse(text) : parseYaml(text)) as Spec;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function generateOne(source: ApiSource, contentRoot: string): Promise<string[]> {
|
|
519
|
+
const spec = await loadSpec(source.spec);
|
|
520
|
+
const baseUrl = spec.servers?.[0]?.url ?? '';
|
|
521
|
+
const group = source.output || (source.title ? slugify(source.title) : 'api-reference');
|
|
522
|
+
const root = join(contentRoot, group);
|
|
523
|
+
rmSync(root, { recursive: true, force: true });
|
|
524
|
+
|
|
525
|
+
const byTag = new Map<string, Page[]>();
|
|
526
|
+
const collect = (entries: Record<string, PathItem> | undefined, defaultTag: string) => {
|
|
527
|
+
for (const [path, item] of Object.entries(entries ?? {})) {
|
|
528
|
+
for (const method of METHODS) {
|
|
529
|
+
const op = item[method];
|
|
530
|
+
if (!op) continue;
|
|
531
|
+
const page = operationPage(spec, baseUrl, group, path, method, op, item, defaultTag);
|
|
532
|
+
if (!byTag.has(page.tag)) byTag.set(page.tag, []);
|
|
533
|
+
byTag.get(page.tag)!.push(page);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
collect(spec.paths, 'default');
|
|
538
|
+
collect(spec.webhooks, 'webhooks');
|
|
539
|
+
|
|
540
|
+
const written: string[] = [];
|
|
541
|
+
const write = (file: string, content: string) => {
|
|
542
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
543
|
+
writeFileSync(file, content);
|
|
544
|
+
written.push(file);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const tags = [...byTag.keys()];
|
|
548
|
+
const schemas = Object.entries(spec.components?.schemas ?? {});
|
|
549
|
+
const sections = [...tags, ...(schemas.length ? ['schemas'] : [])];
|
|
550
|
+
const tagTitle = (t: string): string =>
|
|
551
|
+
titleCase(spec.tags?.find((x) => slugify(x.name) === t)?.name ?? t);
|
|
552
|
+
|
|
553
|
+
write(
|
|
554
|
+
join(root, 'meta.json'),
|
|
555
|
+
JSON.stringify(
|
|
556
|
+
{ title: source.title ?? spec.info?.title ?? 'API Reference', icon: 'code', pages: sections },
|
|
557
|
+
null,
|
|
558
|
+
'\t'
|
|
559
|
+
)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
for (const [tag, pages] of byTag) {
|
|
563
|
+
write(
|
|
564
|
+
join(root, tag, 'meta.json'),
|
|
565
|
+
JSON.stringify({ title: tagTitle(tag), pages: pages.map((p) => p.slug) }, null, '\t')
|
|
566
|
+
);
|
|
567
|
+
for (const page of pages) write(join(root, tag, `${page.slug}.md`), page.body);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (schemas.length) {
|
|
571
|
+
write(
|
|
572
|
+
join(root, 'schemas', 'meta.json'),
|
|
573
|
+
JSON.stringify(
|
|
574
|
+
{ title: 'Schemas', icon: 'box', pages: schemas.map(([name]) => slugify(name)) },
|
|
575
|
+
null,
|
|
576
|
+
'\t'
|
|
577
|
+
)
|
|
578
|
+
);
|
|
579
|
+
for (const [name, schema] of schemas) {
|
|
580
|
+
write(join(root, 'schemas', `${slugify(name)}.md`), schemaPage(spec, group, name, schema));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return written;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Keep generated reference folders out of git (and prettier/eslint, which read
|
|
588
|
+
// .gitignore) without touching the rest of the file.
|
|
589
|
+
function ignoreGenerated(contentRoot: string, groups: string[]): void {
|
|
590
|
+
const file = '.gitignore';
|
|
591
|
+
const start = '# axerity:openapi (generated)';
|
|
592
|
+
const end = '# end axerity:openapi';
|
|
593
|
+
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
594
|
+
const block = [start, ...groups.map((g) => `${contentRoot}/${g}`), end].join('\n');
|
|
595
|
+
let content = existsSync(file) ? readFileSync(file, 'utf8') : '';
|
|
596
|
+
content = content.replace(new RegExp(`\\n*${esc(start)}[\\s\\S]*?${esc(end)}\\n*`, 'g'), '\n');
|
|
597
|
+
if (groups.length) content = `${content.replace(/\n+$/, '')}\n\n${block}\n`;
|
|
598
|
+
writeFileSync(file, content);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Generate one or more API references (local file or URL, JSON or YAML) into the
|
|
603
|
+
* content folder. An array of sources produces a separate group per spec.
|
|
604
|
+
*/
|
|
605
|
+
export async function generateApiDocs(
|
|
606
|
+
openapi: string | ApiSource | ApiSource[] | undefined,
|
|
607
|
+
contentRoot: string
|
|
608
|
+
): Promise<string[]> {
|
|
609
|
+
const sources = (openapi ? (Array.isArray(openapi) ? openapi : [openapi]) : []).map((o) =>
|
|
610
|
+
typeof o === 'string' ? { spec: o } : o
|
|
611
|
+
);
|
|
612
|
+
const written: string[] = [];
|
|
613
|
+
const groups: string[] = [];
|
|
614
|
+
for (const source of sources) {
|
|
615
|
+
if (!source.spec) continue;
|
|
616
|
+
if (!/^https?:\/\//.test(source.spec) && !existsSync(source.spec)) continue;
|
|
617
|
+
groups.push(source.output || (source.title ? slugify(source.title) : 'api-reference'));
|
|
618
|
+
try {
|
|
619
|
+
written.push(...(await generateOne(source, contentRoot)));
|
|
620
|
+
} catch (error) {
|
|
621
|
+
console.warn(`[axerity] OpenAPI generation failed for ${source.spec}:`, error);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// When the CLI mounts a user's project into the engine, generated pages live
|
|
625
|
+
// engine-side and never touch the user's repo, so the .gitignore block is
|
|
626
|
+
// pointless — and rewriting .gitignore mid-run is dangerous. Skip it then.
|
|
627
|
+
if (!process.env.AXERITY_MOUNTED) ignoreGenerated(contentRoot, groups);
|
|
628
|
+
return written;
|
|
629
|
+
}
|