@umami/shiso 1.18.0 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/App.js +1081 -47
- package/dist/chunks/architectureDiagram-5GKGNRK7.js +1 -1
- package/dist/chunks/chunk-GMAD6QVW.js +1 -1
- package/dist/chunks/cose-bilkent-JH36ORCC.js +1 -1
- package/dist/chunks/dist.js +1 -1
- package/dist/chunks/docs.js +183 -8
- package/dist/chunks/ganttDiagram-EL5Y4UJY.js +1 -1
- package/dist/chunks/src.js +1 -1
- package/dist/components.js +1 -1
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +1 -1
- package/docs.schema.json +1164 -1135
- package/package.json +1 -2
- package/scripts/check-content.mjs +42 -15
- package/scripts/expand-openapi-navigation.mjs +47 -21
- package/scripts/generate-openapi.mjs +36 -11
- package/scripts/generate-search-index.mjs +19 -18
- package/scripts/lib/openapi-project.mjs +197 -0
- package/scripts/lib/openapi.mjs +257 -111
- package/scripts/lib/request-samples.mjs +323 -0
- package/scripts/load-docs-config.mjs +23 -15
- package/scripts/prerender.mjs +17 -14
- package/scripts/vite-docs-config.mjs +1 -0
- package/src/components/ApiPlayground.tsx +522 -0
- package/src/components/DocContent.tsx +15 -4
- package/src/components/Docs.tsx +18 -3
- package/src/components/LanguageSwitcher.tsx +26 -30
- package/src/components/OpenApiOperation.tsx +58 -12
- package/src/components/OpenApiSchema.tsx +97 -0
- package/src/components/SideNav.tsx +2 -2
- package/src/lib/openapi.generated.ts +2 -1
- package/src/lib/openapi.ts +104 -10
- package/src/lib/site-model.ts +12 -0
- package/src/lib/translations/de.json +26 -1
- package/src/lib/translations/en.json +26 -1
- package/src/lib/translations/es.json +26 -1
- package/src/lib/translations/fr.json +26 -1
- package/src/lib/translations/ja.json +26 -1
- package/src/lib/translations/zh-Hans.json +26 -1
- package/src/lib/translations/zh-Hant.json +26 -1
- package/src/lib/types.ts +89 -5
- package/types/labels.d.ts +25 -0
- package/vite.config.ts +3 -4
- package/CHANGELOG.md +0 -8
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request building shared by the build-time sample generator and the browser
|
|
3
|
+
* API playground. This module is deliberately free of Node imports: it is
|
|
4
|
+
* bundled into the client runtime as well as executed by the build scripts.
|
|
5
|
+
*
|
|
6
|
+
* `buildRequest` turns an operation plus user-supplied values into a concrete
|
|
7
|
+
* HTTP request description; `buildCodeSamples` renders that description as
|
|
8
|
+
* cURL, JavaScript, and Python. Values that are missing fall back to the
|
|
9
|
+
* spec's examples or to angle-bracket placeholders such as `<token>`, so the
|
|
10
|
+
* static samples and the live playground always agree on the request shape.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PLACEHOLDER = /^<[^<>]+>$/;
|
|
14
|
+
|
|
15
|
+
/** Percent-encodes a value unless it is a documentation placeholder. */
|
|
16
|
+
function encodeValue(value) {
|
|
17
|
+
return PLACEHOLDER.test(value) ? value : encodeURIComponent(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toBase64(value) {
|
|
21
|
+
if (typeof btoa === 'function') return btoa(value);
|
|
22
|
+
return Buffer.from(value, 'utf8').toString('base64');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function capitalize(value) {
|
|
26
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Placeholder credential text shown in samples when no value is supplied. */
|
|
30
|
+
export function securityPlaceholder(scheme) {
|
|
31
|
+
if (scheme.type === 'http') {
|
|
32
|
+
return scheme.scheme === 'basic' ? '<credentials>' : '<token>';
|
|
33
|
+
}
|
|
34
|
+
if (scheme.type === 'apiKey') return '<api-key>';
|
|
35
|
+
return '<access-token>';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Applies one security scheme to the request. `value` is a string for token
|
|
40
|
+
* and API key schemes, `{ username, password }` for HTTP basic, or undefined
|
|
41
|
+
* to emit a placeholder.
|
|
42
|
+
*/
|
|
43
|
+
export function applySecurity(scheme, value, target) {
|
|
44
|
+
const placeholder = securityPlaceholder(scheme);
|
|
45
|
+
|
|
46
|
+
if (scheme.type === 'apiKey') {
|
|
47
|
+
const credential = typeof value === 'string' && value ? value : placeholder;
|
|
48
|
+
const name = scheme.paramName || scheme.name;
|
|
49
|
+
if (scheme.in === 'query') target.query[name] = credential;
|
|
50
|
+
else if (scheme.in === 'cookie') target.cookies[name] = credential;
|
|
51
|
+
else target.headers[name] = credential;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (scheme.type === 'http' && scheme.scheme === 'basic') {
|
|
56
|
+
const credential =
|
|
57
|
+
value && typeof value === 'object' && (value.username || value.password)
|
|
58
|
+
? toBase64(`${value.username || ''}:${value.password || ''}`)
|
|
59
|
+
: typeof value === 'string' && value
|
|
60
|
+
? value
|
|
61
|
+
: placeholder;
|
|
62
|
+
target.headers.Authorization = `Basic ${credential}`;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const prefix =
|
|
67
|
+
scheme.type === 'http' && scheme.scheme && scheme.scheme !== 'bearer'
|
|
68
|
+
? capitalize(scheme.scheme)
|
|
69
|
+
: 'Bearer';
|
|
70
|
+
const credential = typeof value === 'string' && value ? value : placeholder;
|
|
71
|
+
target.headers.Authorization = `${prefix} ${credential}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Content categories the sample renderers and playground understand. */
|
|
75
|
+
export function bodyKind(contentType) {
|
|
76
|
+
if (!contentType) return 'none';
|
|
77
|
+
if (/[+/]json\b/i.test(contentType)) return 'json';
|
|
78
|
+
if (/x-www-form-urlencoded/i.test(contentType)) return 'form';
|
|
79
|
+
if (/multipart\/form-data/i.test(contentType)) return 'multipart';
|
|
80
|
+
return 'raw';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseFields(body) {
|
|
84
|
+
if (!body) return {};
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(body);
|
|
87
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
88
|
+
return Object.fromEntries(
|
|
89
|
+
Object.entries(parsed).map(([key, value]) => [
|
|
90
|
+
key,
|
|
91
|
+
typeof value === 'string' ? value : JSON.stringify(value),
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
94
|
+
} catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parameterValue(node, supplied) {
|
|
100
|
+
if (supplied !== undefined && supplied !== null && supplied !== '') return String(supplied);
|
|
101
|
+
return node.example;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Builds a concrete request from an operation and optional user values:
|
|
106
|
+
* `{ server, path, query, header, cookie, auth, body }`. Without values, the
|
|
107
|
+
* spec's examples fill required parameters and credentials become
|
|
108
|
+
* placeholders, which is the shape rendered in the static code samples.
|
|
109
|
+
*/
|
|
110
|
+
export function buildRequest(operation, values = {}) {
|
|
111
|
+
const live = values.live === true;
|
|
112
|
+
const target = { headers: {}, query: {}, cookies: {} };
|
|
113
|
+
|
|
114
|
+
for (const scheme of operation.security || []) {
|
|
115
|
+
applySecurity(scheme, values.auth?.[scheme.name], target);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const params = operation.parameters;
|
|
119
|
+
const pathValues = {};
|
|
120
|
+
for (const node of params.path) {
|
|
121
|
+
const value = parameterValue(node, values.path?.[node.name]);
|
|
122
|
+
// Static samples keep `{id}` placeholders so readers recognize the slot.
|
|
123
|
+
if (live && value !== undefined) pathValues[node.name] = value;
|
|
124
|
+
}
|
|
125
|
+
for (const node of params.query) {
|
|
126
|
+
const value = parameterValue(node, values.query?.[node.name]);
|
|
127
|
+
if (value !== undefined && (live || node.required || values.query?.[node.name])) {
|
|
128
|
+
target.query[node.name] = value;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const node of params.header) {
|
|
132
|
+
const value = parameterValue(node, values.header?.[node.name]);
|
|
133
|
+
if (value !== undefined && (live || node.required || values.header?.[node.name])) {
|
|
134
|
+
target.headers[node.name] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const node of params.cookie) {
|
|
138
|
+
const value = parameterValue(node, values.cookie?.[node.name]);
|
|
139
|
+
if (value !== undefined && (live || node.required || values.cookie?.[node.name])) {
|
|
140
|
+
target.cookies[node.name] = value;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const server = (values.server || operation.servers?.[0]?.url || operation.serverUrl || '')
|
|
145
|
+
.trim()
|
|
146
|
+
.replace(/\/+$/, '');
|
|
147
|
+
const pathname = operation.path.replace(/\{([^}]+)\}/g, (match, name) =>
|
|
148
|
+
pathValues[name] !== undefined ? encodeURIComponent(pathValues[name]) : match,
|
|
149
|
+
);
|
|
150
|
+
const query = Object.entries(target.query)
|
|
151
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeValue(value)}`)
|
|
152
|
+
.join('&');
|
|
153
|
+
const url = `${server}${pathname}${query ? `?${query}` : ''}`;
|
|
154
|
+
|
|
155
|
+
const cookieHeader = Object.entries(target.cookies)
|
|
156
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
157
|
+
.join('; ');
|
|
158
|
+
if (cookieHeader) target.headers.Cookie = cookieHeader;
|
|
159
|
+
|
|
160
|
+
const contentType = operation.requestBody?.contentType;
|
|
161
|
+
const kind = operation.requestBody ? bodyKind(contentType) : 'none';
|
|
162
|
+
const body = values.body !== undefined ? values.body : operation.requestBody?.example;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
method: operation.method,
|
|
166
|
+
url,
|
|
167
|
+
headers: target.headers,
|
|
168
|
+
contentType,
|
|
169
|
+
bodyKind: kind,
|
|
170
|
+
body: kind === 'none' ? undefined : body,
|
|
171
|
+
fields: kind === 'form' || kind === 'multipart' ? parseFields(body) : undefined,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function shellQuote(value) {
|
|
176
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function jsString(value) {
|
|
180
|
+
return `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n')}'`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function jsKey(key) {
|
|
184
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : jsString(key);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function pythonString(value) {
|
|
188
|
+
return jsString(value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pythonLiteral(json) {
|
|
192
|
+
// Request bodies are JSON strings; the Python sample shows them as dict
|
|
193
|
+
// literals, which differ from JSON only in quoting and keyword spelling.
|
|
194
|
+
return json
|
|
195
|
+
.replace(/"/g, "'")
|
|
196
|
+
.replace(/\btrue\b/g, 'True')
|
|
197
|
+
.replace(/\bfalse\b/g, 'False')
|
|
198
|
+
.replace(/\bnull\b/g, 'None');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function indentLines(text, indent) {
|
|
202
|
+
return text
|
|
203
|
+
.split('\n')
|
|
204
|
+
.map((line, index) => (index === 0 ? line : `${indent}${line}`))
|
|
205
|
+
.join('\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function curlSample(request) {
|
|
209
|
+
const { method, url, headers, bodyKind: kind, body, fields, contentType } = request;
|
|
210
|
+
const lines = [`curl -X ${method} ${shellQuote(url)}`];
|
|
211
|
+
|
|
212
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
213
|
+
lines.push(` -H ${shellQuote(`${key}: ${value}`)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (kind === 'json' && body) {
|
|
217
|
+
lines.push(` -H ${shellQuote(`Content-Type: ${contentType}`)}`, ` -d ${shellQuote(body)}`);
|
|
218
|
+
} else if (kind === 'form') {
|
|
219
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
220
|
+
lines.push(` --data-urlencode ${shellQuote(`${key}=${value}`)}`);
|
|
221
|
+
}
|
|
222
|
+
} else if (kind === 'multipart') {
|
|
223
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
224
|
+
lines.push(` -F ${shellQuote(`${key}=${value}`)}`);
|
|
225
|
+
}
|
|
226
|
+
} else if (kind === 'raw' && body) {
|
|
227
|
+
lines.push(` -H ${shellQuote(`Content-Type: ${contentType}`)}`, ` -d ${shellQuote(body)}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return lines.join(' \\\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function javascriptSample(request) {
|
|
234
|
+
const { method, url, headers, bodyKind: kind, body, fields, contentType } = request;
|
|
235
|
+
const headerLines = Object.entries(headers).map(
|
|
236
|
+
([key, value]) => ` ${jsKey(key)}: ${jsString(value)},`,
|
|
237
|
+
);
|
|
238
|
+
if ((kind === 'json' || kind === 'raw') && body) {
|
|
239
|
+
headerLines.unshift(` 'Content-Type': ${jsString(contentType)},`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const preamble = [];
|
|
243
|
+
let bodyLine;
|
|
244
|
+
if (kind === 'json' && body) {
|
|
245
|
+
bodyLine = ` body: JSON.stringify(${indentLines(body, ' ')}),`;
|
|
246
|
+
} else if (kind === 'form') {
|
|
247
|
+
const entries = Object.entries(fields || {})
|
|
248
|
+
.map(([key, value]) => ` ${jsKey(key)}: ${jsString(value)},`)
|
|
249
|
+
.join('\n');
|
|
250
|
+
bodyLine = entries
|
|
251
|
+
? ` body: new URLSearchParams({\n${entries}\n }),`
|
|
252
|
+
: ' body: new URLSearchParams(),';
|
|
253
|
+
} else if (kind === 'multipart') {
|
|
254
|
+
preamble.push('const body = new FormData();');
|
|
255
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
256
|
+
preamble.push(`body.append(${jsString(key)}, ${jsString(value)});`);
|
|
257
|
+
}
|
|
258
|
+
preamble.push('');
|
|
259
|
+
bodyLine = ' body,';
|
|
260
|
+
} else if (kind === 'raw' && body) {
|
|
261
|
+
bodyLine = ` body: ${jsString(body)},`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return [
|
|
265
|
+
...preamble,
|
|
266
|
+
`const response = await fetch(${jsString(url)}, {`,
|
|
267
|
+
` method: ${jsString(method)},`,
|
|
268
|
+
...(headerLines.length ? [' headers: {', ...headerLines, ' },'] : []),
|
|
269
|
+
...(bodyLine ? [bodyLine] : []),
|
|
270
|
+
'});',
|
|
271
|
+
'const data = await response.json();',
|
|
272
|
+
].join('\n');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function pythonSample(request) {
|
|
276
|
+
const { method, url, headers, bodyKind: kind, body, fields, contentType } = request;
|
|
277
|
+
const headerEntries = Object.entries(headers);
|
|
278
|
+
if (kind === 'raw' && body) headerEntries.unshift(['Content-Type', contentType]);
|
|
279
|
+
const headerLine = headerEntries.length
|
|
280
|
+
? ` headers={${headerEntries
|
|
281
|
+
.map(([key, value]) => `${pythonString(key)}: ${pythonString(value)}`)
|
|
282
|
+
.join(', ')}},`
|
|
283
|
+
: undefined;
|
|
284
|
+
|
|
285
|
+
let bodyLine;
|
|
286
|
+
if (kind === 'json' && body) {
|
|
287
|
+
bodyLine = ` json=${indentLines(pythonLiteral(body), ' ')},`;
|
|
288
|
+
} else if (kind === 'form') {
|
|
289
|
+
const entries = Object.entries(fields || {})
|
|
290
|
+
.map(([key, value]) => `${pythonString(key)}: ${pythonString(value)}`)
|
|
291
|
+
.join(', ');
|
|
292
|
+
bodyLine = ` data={${entries}},`;
|
|
293
|
+
} else if (kind === 'multipart') {
|
|
294
|
+
const entries = Object.entries(fields || {})
|
|
295
|
+
.map(([key, value]) => `${pythonString(key)}: (None, ${pythonString(value)})`)
|
|
296
|
+
.join(', ');
|
|
297
|
+
bodyLine = ` files={${entries}},`;
|
|
298
|
+
} else if (kind === 'raw' && body) {
|
|
299
|
+
bodyLine = ` data=${pythonString(body)},`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [
|
|
303
|
+
'import requests',
|
|
304
|
+
'',
|
|
305
|
+
`response = requests.${method.toLowerCase()}(`,
|
|
306
|
+
` ${pythonString(url)},`,
|
|
307
|
+
...(headerLine ? [headerLine] : []),
|
|
308
|
+
...(bodyLine ? [bodyLine] : []),
|
|
309
|
+
')',
|
|
310
|
+
'print(response.json())',
|
|
311
|
+
].join('\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Builds cURL, JavaScript, and Python request samples for an operation. */
|
|
315
|
+
export function buildCodeSamples(operation, values) {
|
|
316
|
+
const request = buildRequest(operation, values);
|
|
317
|
+
|
|
318
|
+
return [
|
|
319
|
+
{ language: 'bash', label: 'cURL', source: curlSample(request) },
|
|
320
|
+
{ language: 'javascript', label: 'JavaScript', source: javascriptSample(request) },
|
|
321
|
+
{ language: 'python', label: 'Python', source: pythonSample(request) },
|
|
322
|
+
];
|
|
323
|
+
}
|
|
@@ -2,12 +2,8 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { expandNavigationGlobs, hasNavigationGlobs } from './expand-navigation-globs.mjs';
|
|
4
4
|
import { expandOpenApiNavigation, hasOpenApiItems } from './expand-openapi-navigation.mjs';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
loadOpenApiSpec,
|
|
8
|
-
normalizeOperations,
|
|
9
|
-
resolveApiDirectory,
|
|
10
|
-
} from './lib/openapi.mjs';
|
|
5
|
+
import { generateOpenApiStubs } from './lib/openapi.mjs';
|
|
6
|
+
import { loadApiProject } from './lib/openapi-project.mjs';
|
|
11
7
|
import { loadShisoConfig } from './load-shiso-config.mjs';
|
|
12
8
|
|
|
13
9
|
/** Error raised while locating, reading, or parsing a Shiso configuration file. */
|
|
@@ -244,24 +240,27 @@ export async function loadDocsConfig({
|
|
|
244
240
|
const sourcePath = sourcePaths[0] || requestedSourcePath;
|
|
245
241
|
const hasGlobs = hasNavigationGlobs(sourceConfig.navigation);
|
|
246
242
|
let working = sourceConfig;
|
|
247
|
-
let
|
|
243
|
+
let specPaths = [];
|
|
248
244
|
|
|
249
245
|
// OpenAPI expansion runs before glob expansion so generated stub pages are
|
|
250
246
|
// visible to navigation globs and every downstream consumer.
|
|
251
247
|
if (expandGlobs && working.api?.spec) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
const operations = normalizeOperations(loadedSpec.spec);
|
|
255
|
-
const directory = resolveApiDirectory(working.api);
|
|
248
|
+
const project = await loadApiProject({ root: projectRoot, api: working.api });
|
|
249
|
+
specPaths = project.specPaths;
|
|
256
250
|
const { config: shisoConfig } = await loadShisoConfig({ root: projectRoot });
|
|
257
251
|
|
|
258
252
|
await generateOpenApiStubs({
|
|
259
253
|
root: projectRoot,
|
|
260
254
|
contentDir: shisoConfig.contentDir,
|
|
261
|
-
directory,
|
|
262
|
-
operations,
|
|
255
|
+
directory: project.directory,
|
|
256
|
+
operations: project.operations,
|
|
257
|
+
prefixSpec: project.multi,
|
|
258
|
+
});
|
|
259
|
+
working = expandOpenApiNavigation(working, {
|
|
260
|
+
operations: project.operations,
|
|
261
|
+
directory: project.directory,
|
|
262
|
+
specs: project.specs,
|
|
263
263
|
});
|
|
264
|
-
working = expandOpenApiNavigation(working, { operations, directory });
|
|
265
264
|
} else if (expandGlobs && hasOpenApiItems(working.navigation)) {
|
|
266
265
|
throw new Error(
|
|
267
266
|
'Navigation contains an { "openapi" } entry but docs.json has no "api.spec" setting.',
|
|
@@ -275,7 +274,16 @@ export async function loadDocsConfig({
|
|
|
275
274
|
})
|
|
276
275
|
: working;
|
|
277
276
|
|
|
278
|
-
return {
|
|
277
|
+
return {
|
|
278
|
+
config,
|
|
279
|
+
projectRoot,
|
|
280
|
+
sourcePath,
|
|
281
|
+
sourcePaths,
|
|
282
|
+
hasGlobs,
|
|
283
|
+
specPaths,
|
|
284
|
+
/** @deprecated use specPaths */
|
|
285
|
+
specPath: specPaths[0],
|
|
286
|
+
};
|
|
279
287
|
}
|
|
280
288
|
|
|
281
289
|
export async function loadDocsSchema({
|
package/scripts/prerender.mjs
CHANGED
|
@@ -16,12 +16,8 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import process from 'node:process';
|
|
18
18
|
import { pathToFileURL } from 'node:url';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
normalizeOperationKey,
|
|
22
|
-
normalizeOperations,
|
|
23
|
-
operationToMarkdown,
|
|
24
|
-
} from './lib/openapi.mjs';
|
|
19
|
+
import { operationToMarkdown, schemaToMarkdown } from './lib/openapi.mjs';
|
|
20
|
+
import { loadApiProject, lookupOperation, lookupSchema } from './lib/openapi-project.mjs';
|
|
25
21
|
import { loadDocsConfig } from './load-docs-config.mjs';
|
|
26
22
|
|
|
27
23
|
const DEFAULT_HEAD_OPEN = '<!--shiso-default-head-->';
|
|
@@ -122,21 +118,28 @@ if (docsHomeUrl && docsHomeUrl !== '/' && !routes.includes('/')) {
|
|
|
122
118
|
|
|
123
119
|
// Pages bound to an API operation publish the generated reference as markdown
|
|
124
120
|
// too, so the .md copies and llms-full.txt stay useful to AI tools.
|
|
125
|
-
let
|
|
121
|
+
let apiProject;
|
|
126
122
|
{
|
|
127
123
|
const docsConfig = (await loadDocsConfig({ root, expandGlobs: false })).config;
|
|
128
124
|
if (docsConfig.api?.spec) {
|
|
129
|
-
|
|
130
|
-
openApiByKey = new Map(normalizeOperations(spec).map(operation => [operation.key, operation]));
|
|
125
|
+
apiProject = await loadApiProject({ root, api: docsConfig.api });
|
|
131
126
|
}
|
|
132
127
|
}
|
|
133
128
|
|
|
134
|
-
function
|
|
135
|
-
if (!openApiByKey) return source;
|
|
129
|
+
function frontmatterValue(source, name) {
|
|
136
130
|
const frontmatter = source.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1] || '';
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
return frontmatter
|
|
132
|
+
.match(new RegExp(`^${name}:\\s*(.+)$`, 'm'))?.[1]
|
|
133
|
+
?.trim()
|
|
134
|
+
.replace(/^["']|["']$/g, '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function withOperationMarkdown(source) {
|
|
138
|
+
if (!apiProject) return source;
|
|
139
|
+
const operation = lookupOperation(apiProject, frontmatterValue(source, 'openapi'));
|
|
140
|
+
if (operation) return `${source.trimEnd()}\n\n${operationToMarkdown(operation)}\n`;
|
|
141
|
+
const schema = lookupSchema(apiProject, frontmatterValue(source, 'openapi-schema'));
|
|
142
|
+
return schema ? `${source.trimEnd()}\n\n${schemaToMarkdown(schema)}\n` : source;
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
// Raw markdown next to every page: "/docs/installation" -> "docs/installation.md".
|
|
@@ -43,6 +43,7 @@ export async function createDocsConfigModule({
|
|
|
43
43
|
getConfig: () => loaded.config,
|
|
44
44
|
getShisoConfig: () => loadedShiso.config,
|
|
45
45
|
getSpecPath: () => loaded.specPath,
|
|
46
|
+
getSpecPaths: () => loaded.specPaths || [],
|
|
46
47
|
getSourcePaths: () => [...loaded.sourcePaths, ...loadedShiso.sourcePaths],
|
|
47
48
|
sourcePath: loaded.sourcePath,
|
|
48
49
|
shisoSourcePath: loadedShiso.sourcePath,
|