@umami/shiso 1.17.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 +1217 -196
- 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 +843 -22
- 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 -1131
- package/package.json +1 -2
- package/scripts/check-content.mjs +44 -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/load-shiso-config.mjs +30 -1
- package/scripts/prerender.mjs +17 -14
- package/scripts/vite-docs-config.mjs +1 -0
- package/src/App.tsx +32 -22
- package/src/components/ApiPlayground.tsx +522 -0
- package/src/components/CodeBlock.tsx +3 -1
- package/src/components/DocContent.tsx +15 -4
- package/src/components/Docs.tsx +19 -2
- package/src/components/Footer.tsx +3 -1
- package/src/components/Header.tsx +4 -3
- package/src/components/LanguageSwitcher.tsx +38 -29
- package/src/components/OpenApiOperation.tsx +73 -20
- package/src/components/OpenApiSchema.tsx +97 -0
- package/src/components/PageActions.tsx +8 -7
- package/src/components/SideNav.tsx +2 -2
- package/src/components/docs/Changelog.tsx +5 -5
- package/src/components/docs/CodeGroup.tsx +3 -1
- package/src/components/docs/Mermaid.tsx +8 -6
- package/src/components/docs/PropertiesTable.tsx +11 -6
- package/src/components/docs/Tabs.tsx +3 -1
- package/src/components/docs/Tree.tsx +3 -1
- package/src/components/docs/ZoomableImage.tsx +4 -2
- package/src/components/ui/dialog.tsx +7 -2
- package/src/components/ui/sheet.tsx +5 -2
- package/src/lib/label-context.tsx +7 -0
- package/src/lib/labels.ts +37 -0
- package/src/lib/openapi.generated.ts +2 -1
- package/src/lib/openapi.ts +123 -16
- package/src/lib/site-config.ts +66 -2
- package/src/lib/site-model.ts +14 -32
- package/src/lib/standalone-pages.ts +15 -1
- package/src/lib/translations/de.json +99 -0
- package/src/lib/translations/en.json +99 -0
- package/src/lib/translations/es.json +99 -0
- package/src/lib/translations/fr.json +99 -0
- package/src/lib/translations/ja.json +99 -0
- package/src/lib/translations/zh-Hans.json +99 -0
- package/src/lib/translations/zh-Hant.json +99 -0
- package/src/lib/types.ts +109 -37
- package/types/config.d.ts +7 -1
- package/types/labels.d.ts +101 -0
- package/vite.config.ts +3 -4
- package/CHANGELOG.md +0 -8
package/scripts/lib/openapi.mjs
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
import fs from 'node:fs/promises';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import { parse as parseYaml } from 'yaml';
|
|
14
|
+
import { buildCodeSamples } from './request-samples.mjs';
|
|
14
15
|
import { slugify } from './slug.mjs';
|
|
15
16
|
|
|
17
|
+
export { buildCodeSamples, buildRequest } from './request-samples.mjs';
|
|
18
|
+
|
|
16
19
|
const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
|
|
17
20
|
const MAX_DEPTH = 8;
|
|
18
21
|
const MAX_CHILDREN = 100;
|
|
@@ -287,21 +290,68 @@ function formatExample(value) {
|
|
|
287
290
|
return value === undefined ? undefined : JSON.stringify(value, null, 2);
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
|
|
293
|
+
const SECURITY_TYPES = new Set(['http', 'apiKey', 'oauth2', 'openIdConnect']);
|
|
294
|
+
|
|
295
|
+
/** Human-readable label for a security scheme, e.g. "bearerAuth (http bearer)". */
|
|
296
|
+
export function securityLabel(scheme) {
|
|
297
|
+
const detail = [scheme.type !== 'unknown' ? scheme.type : undefined, scheme.scheme]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join(' ');
|
|
300
|
+
return detail ? `${scheme.name} (${detail})` : scheme.name;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Resolves the security schemes an operation accepts. Every scheme named by
|
|
305
|
+
* any requirement alternative is listed once, in declaration order, so the
|
|
306
|
+
* playground can offer an input for each and samples show how it is sent.
|
|
307
|
+
*/
|
|
308
|
+
function securitySchemes(spec, operation) {
|
|
291
309
|
const requirements = operation.security ?? spec.security ?? [];
|
|
292
|
-
const
|
|
293
|
-
const
|
|
310
|
+
const definitions = spec.components?.securitySchemes || {};
|
|
311
|
+
const schemes = new Map();
|
|
294
312
|
|
|
295
313
|
for (const requirement of requirements) {
|
|
296
314
|
for (const name of Object.keys(requirement || {})) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
315
|
+
if (schemes.has(name)) continue;
|
|
316
|
+
const definition = deref(spec, definitions[name]);
|
|
317
|
+
const type = SECURITY_TYPES.has(definition?.type) ? definition.type : 'unknown';
|
|
318
|
+
const scheme = { name, type };
|
|
319
|
+
if (type === 'http' && typeof definition.scheme === 'string') {
|
|
320
|
+
scheme.scheme = definition.scheme.toLowerCase();
|
|
321
|
+
}
|
|
322
|
+
if (type === 'apiKey') {
|
|
323
|
+
scheme.in = ['query', 'cookie'].includes(definition.in) ? definition.in : 'header';
|
|
324
|
+
scheme.paramName = definition.name || name;
|
|
325
|
+
}
|
|
326
|
+
if (typeof definition?.description === 'string') scheme.description = definition.description;
|
|
327
|
+
scheme.label = securityLabel(scheme);
|
|
328
|
+
schemes.set(name, scheme);
|
|
301
329
|
}
|
|
302
330
|
}
|
|
303
331
|
|
|
304
|
-
return [...
|
|
332
|
+
return [...schemes.values()];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Fills server URL variables with their default values. */
|
|
336
|
+
function expandServerUrl(server) {
|
|
337
|
+
return String(server.url || '').replace(/\{([^}]+)\}/g, (match, name) => {
|
|
338
|
+
const variable = server.variables?.[name];
|
|
339
|
+
return variable?.default !== undefined ? String(variable.default) : match;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Servers for an operation: operation-level, then path-level, then global. */
|
|
344
|
+
function resolveServers(spec, pathItem, operation) {
|
|
345
|
+
const list = [operation.servers, pathItem.servers, spec.servers].find(
|
|
346
|
+
candidate => Array.isArray(candidate) && candidate.length > 0,
|
|
347
|
+
);
|
|
348
|
+
const servers = (list || [])
|
|
349
|
+
.filter(server => server && typeof server === 'object' && typeof server.url === 'string')
|
|
350
|
+
.map(server => ({
|
|
351
|
+
url: expandServerUrl(server),
|
|
352
|
+
...(server.description ? { description: server.description } : {}),
|
|
353
|
+
}));
|
|
354
|
+
return servers.length ? servers : [{ url: FALLBACK_SERVER }];
|
|
305
355
|
}
|
|
306
356
|
|
|
307
357
|
function parameterNode(spec, parameter) {
|
|
@@ -312,36 +362,57 @@ function parameterNode(spec, parameter) {
|
|
|
312
362
|
});
|
|
313
363
|
if (resolved.description && !node.description) node.description = resolved.description;
|
|
314
364
|
if (resolved.deprecated === true) node.deprecated = true;
|
|
315
|
-
|
|
365
|
+
const example =
|
|
366
|
+
resolved.example !== undefined
|
|
367
|
+
? resolved.example
|
|
368
|
+
: exampleFromSchema(spec, resolved.schema || {});
|
|
369
|
+
if (example !== null && example !== undefined) node.example = stringifyValue(example);
|
|
370
|
+
return { location: resolved.in, node };
|
|
316
371
|
}
|
|
317
372
|
|
|
318
|
-
|
|
319
|
-
|
|
373
|
+
const HTTP_METHODS = new Set(METHODS.map(method => method.toUpperCase()));
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Normalizes every operation in the spec into a serializable shape. OpenAPI
|
|
377
|
+
* 3.1 `webhooks` (and the `x-webhooks` extension) become operations flagged
|
|
378
|
+
* `webhook: true`, keyed `WEBHOOK <name>`: they describe payloads the API
|
|
379
|
+
* sends, so they carry no servers, samples, or playground.
|
|
380
|
+
*/
|
|
381
|
+
export function normalizeOperations(spec, { specId } = {}) {
|
|
320
382
|
const operations = [];
|
|
383
|
+
const sources = [
|
|
384
|
+
...Object.entries(spec.paths || {}).map(([name, item]) => [name, item, false]),
|
|
385
|
+
...Object.entries(spec.webhooks || spec['x-webhooks'] || {}).map(([name, item]) => [
|
|
386
|
+
name,
|
|
387
|
+
item,
|
|
388
|
+
true,
|
|
389
|
+
]),
|
|
390
|
+
];
|
|
321
391
|
|
|
322
|
-
for (const [pathName, pathItem] of
|
|
392
|
+
for (const [pathName, pathItem, webhook] of sources) {
|
|
323
393
|
const resolvedPath = deref(spec, pathItem);
|
|
324
394
|
if (!resolvedPath || typeof resolvedPath !== 'object') continue;
|
|
325
395
|
|
|
326
396
|
for (const method of METHODS) {
|
|
327
397
|
const operation = resolvedPath[method];
|
|
328
398
|
if (!operation || typeof operation !== 'object') continue;
|
|
399
|
+
// A webhook name maps to one page; a second method on the same
|
|
400
|
+
// webhook would collide with it, so only the first is documented.
|
|
401
|
+
if (webhook && operations.some(item => item.webhook && item.path === pathName)) break;
|
|
329
402
|
|
|
330
403
|
const upper = method.toUpperCase();
|
|
331
404
|
const parameters = { query: [], path: [], header: [], cookie: [] };
|
|
332
405
|
const merged = [...(resolvedPath.parameters || []), ...(operation.parameters || [])];
|
|
333
406
|
const seenParams = new Set();
|
|
334
|
-
const paramExamples = {};
|
|
335
407
|
|
|
336
408
|
// Operation-level parameters override path-level ones with the same
|
|
337
409
|
// name and location, so walk the merged list from the end.
|
|
338
410
|
for (const parameter of merged.reverse()) {
|
|
339
|
-
const { location, node
|
|
411
|
+
const { location, node } = parameterNode(spec, parameter);
|
|
340
412
|
const dedupeKey = `${location}:${node.name}`;
|
|
341
413
|
if (!parameters[location] || seenParams.has(dedupeKey)) continue;
|
|
342
414
|
seenParams.add(dedupeKey);
|
|
343
415
|
parameters[location].unshift(node);
|
|
344
|
-
if (example !== null && example !== undefined) paramExamples[dedupeKey] = example;
|
|
345
416
|
}
|
|
346
417
|
|
|
347
418
|
const bodySource = deref(spec, operation.requestBody);
|
|
@@ -369,18 +440,22 @@ export function normalizeOperations(spec) {
|
|
|
369
440
|
};
|
|
370
441
|
});
|
|
371
442
|
|
|
372
|
-
const
|
|
443
|
+
const servers = webhook ? [] : resolveServers(spec, resolvedPath, operation);
|
|
444
|
+
const kebab = value =>
|
|
445
|
+
value
|
|
446
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
447
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
448
|
+
.replace(/[_\s.]+/g, '-');
|
|
449
|
+
const idSource = operation.operationId
|
|
450
|
+
? kebab(operation.operationId)
|
|
451
|
+
: webhook
|
|
452
|
+
? `webhook-${kebab(pathName)}`
|
|
453
|
+
: `${method}-${pathName}`;
|
|
373
454
|
const normalized = {
|
|
374
|
-
id: slugify(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
379
|
-
.replace(/[_\s]+/g, '-')
|
|
380
|
-
: `${method}-${pathName}`,
|
|
381
|
-
`${method}-${slugify(pathName, 'root')}`,
|
|
382
|
-
),
|
|
383
|
-
key: `${upper} ${pathName}`,
|
|
455
|
+
id: slugify(idSource, `${webhook ? 'webhook' : method}-${slugify(pathName, 'root')}`),
|
|
456
|
+
key: webhook ? `WEBHOOK ${pathName}` : `${upper} ${pathName}`,
|
|
457
|
+
...(specId ? { spec: specId } : {}),
|
|
458
|
+
...(webhook ? { webhook: true } : {}),
|
|
384
459
|
method: upper,
|
|
385
460
|
path: pathName,
|
|
386
461
|
summary: operation.summary || undefined,
|
|
@@ -390,12 +465,13 @@ export function normalizeOperations(spec) {
|
|
|
390
465
|
parameters,
|
|
391
466
|
requestBody,
|
|
392
467
|
responses,
|
|
393
|
-
security:
|
|
394
|
-
|
|
468
|
+
security: webhook ? [] : securitySchemes(spec, operation),
|
|
469
|
+
servers,
|
|
470
|
+
serverUrl: servers[0]?.url || '',
|
|
395
471
|
samples: [],
|
|
396
472
|
};
|
|
397
473
|
|
|
398
|
-
normalized.samples = buildCodeSamples(normalized
|
|
474
|
+
if (!webhook) normalized.samples = buildCodeSamples(normalized);
|
|
399
475
|
operations.push(normalized);
|
|
400
476
|
}
|
|
401
477
|
}
|
|
@@ -415,70 +491,6 @@ export function normalizeOperations(spec) {
|
|
|
415
491
|
return operations;
|
|
416
492
|
}
|
|
417
493
|
|
|
418
|
-
function queryString(operation, paramExamples) {
|
|
419
|
-
const pairs = operation.parameters.query
|
|
420
|
-
.filter(parameter => parameter.required)
|
|
421
|
-
.map(parameter => {
|
|
422
|
-
const example = paramExamples[`query:${parameter.name}`];
|
|
423
|
-
return `${parameter.name}=${encodeURIComponent(String(example ?? ''))}`;
|
|
424
|
-
});
|
|
425
|
-
return pairs.length ? `?${pairs.join('&')}` : '';
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function pythonLiteral(json) {
|
|
429
|
-
return json
|
|
430
|
-
.replace(/"([^"]+)":/g, "'$1':")
|
|
431
|
-
.replace(/"/g, "'")
|
|
432
|
-
.replace(/\btrue\b/g, 'True')
|
|
433
|
-
.replace(/\bfalse\b/g, 'False')
|
|
434
|
-
.replace(/\bnull\b/g, 'None');
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/** Builds curl, JavaScript, and Python request samples for an operation. */
|
|
438
|
-
export function buildCodeSamples(operation, paramExamples = {}) {
|
|
439
|
-
const url = `${operation.serverUrl.replace(/\/$/, '')}${operation.path}${queryString(operation, paramExamples)}`;
|
|
440
|
-
const hasAuth = operation.security.length > 0;
|
|
441
|
-
const body = operation.requestBody?.example;
|
|
442
|
-
const method = operation.method;
|
|
443
|
-
|
|
444
|
-
const escapedBody = body ? body.replace(/'/g, `'\\''`) : undefined;
|
|
445
|
-
const curl = [
|
|
446
|
-
`curl -X ${method} '${url}'`,
|
|
447
|
-
...(hasAuth ? [` -H 'Authorization: Bearer <token>'`] : []),
|
|
448
|
-
...(body ? [` -H 'Content-Type: application/json'`, ` -d '${escapedBody}'`] : []),
|
|
449
|
-
].join(' \\\n');
|
|
450
|
-
|
|
451
|
-
const headers = [
|
|
452
|
-
...(body ? [` 'Content-Type': 'application/json',`] : []),
|
|
453
|
-
...(hasAuth ? [` Authorization: 'Bearer <token>',`] : []),
|
|
454
|
-
];
|
|
455
|
-
const javascript = [
|
|
456
|
-
`const response = await fetch('${url}', {`,
|
|
457
|
-
` method: '${method}',`,
|
|
458
|
-
...(headers.length ? [' headers: {', ...headers, ' },'] : []),
|
|
459
|
-
...(body ? [` body: JSON.stringify(${body}),`] : []),
|
|
460
|
-
'});',
|
|
461
|
-
'const data = await response.json();',
|
|
462
|
-
].join('\n');
|
|
463
|
-
|
|
464
|
-
const python = [
|
|
465
|
-
'import requests',
|
|
466
|
-
'',
|
|
467
|
-
`response = requests.${method.toLowerCase()}(`,
|
|
468
|
-
` '${url}',`,
|
|
469
|
-
...(hasAuth ? [` headers={'Authorization': 'Bearer <token>'},`] : []),
|
|
470
|
-
...(body ? [` json=${pythonLiteral(body)},`] : []),
|
|
471
|
-
')',
|
|
472
|
-
'print(response.json())',
|
|
473
|
-
].join('\n');
|
|
474
|
-
|
|
475
|
-
return [
|
|
476
|
-
{ language: 'bash', label: 'cURL', source: curl },
|
|
477
|
-
{ language: 'javascript', label: 'JavaScript', source: javascript },
|
|
478
|
-
{ language: 'python', label: 'Python', source: python },
|
|
479
|
-
];
|
|
480
|
-
}
|
|
481
|
-
|
|
482
494
|
function markdownSchemaLines(node, depth = 0) {
|
|
483
495
|
if (!node) return [];
|
|
484
496
|
const indent = ' '.repeat(depth);
|
|
@@ -493,10 +505,18 @@ function markdownSchemaLines(node, depth = 0) {
|
|
|
493
505
|
|
|
494
506
|
/** Renders an operation as markdown for the .md export and llms-full.txt. */
|
|
495
507
|
export function operationToMarkdown(operation) {
|
|
496
|
-
const lines = [
|
|
508
|
+
const lines = [
|
|
509
|
+
operation.webhook
|
|
510
|
+
? `## Webhook: ${operation.path}`
|
|
511
|
+
: `## ${operation.method} ${operation.path}`,
|
|
512
|
+
'',
|
|
513
|
+
];
|
|
497
514
|
|
|
498
515
|
if (operation.summary) lines.push(operation.summary, '');
|
|
499
516
|
if (operation.description) lines.push(operation.description, '');
|
|
517
|
+
if (operation.security.length) {
|
|
518
|
+
lines.push(`Authentication: ${operation.security.map(scheme => scheme.label).join(', ')}`, '');
|
|
519
|
+
}
|
|
500
520
|
|
|
501
521
|
const allParameters = ['path', 'query', 'header', 'cookie'].flatMap(location =>
|
|
502
522
|
operation.parameters[location].map(parameter => ({ location, parameter })),
|
|
@@ -514,7 +534,12 @@ export function operationToMarkdown(operation) {
|
|
|
514
534
|
}
|
|
515
535
|
|
|
516
536
|
if (operation.requestBody) {
|
|
517
|
-
lines.push(
|
|
537
|
+
lines.push(
|
|
538
|
+
operation.webhook ? '### Payload' : '### Request body',
|
|
539
|
+
'',
|
|
540
|
+
...markdownSchemaLines(operation.requestBody.schema),
|
|
541
|
+
'',
|
|
542
|
+
);
|
|
518
543
|
if (operation.requestBody.example) {
|
|
519
544
|
lines.push('```json', operation.requestBody.example, '```', '');
|
|
520
545
|
}
|
|
@@ -541,20 +566,99 @@ export function operationToMarkdown(operation) {
|
|
|
541
566
|
return lines.join('\n').trim();
|
|
542
567
|
}
|
|
543
568
|
|
|
569
|
+
/**
|
|
570
|
+
* Normalizes every named schema under components.schemas into a page-ready
|
|
571
|
+
* shape, keyed by its name (and `<spec> <name>` on multi-spec sites).
|
|
572
|
+
*/
|
|
573
|
+
export function normalizeSchemas(spec, { specId } = {}) {
|
|
574
|
+
const schemas = [];
|
|
575
|
+
|
|
576
|
+
for (const [name, schema] of Object.entries(spec.components?.schemas || {})) {
|
|
577
|
+
if (!schema || typeof schema !== 'object') continue;
|
|
578
|
+
const resolved = deref(spec, schema);
|
|
579
|
+
const example = exampleFromSchema(spec, schema);
|
|
580
|
+
schemas.push({
|
|
581
|
+
name,
|
|
582
|
+
key: name,
|
|
583
|
+
...(specId ? { spec: specId } : {}),
|
|
584
|
+
title: typeof resolved?.title === 'string' ? resolved.title : undefined,
|
|
585
|
+
description: typeof resolved?.description === 'string' ? resolved.description : undefined,
|
|
586
|
+
schema: schemaTree(spec, schema),
|
|
587
|
+
example: formatExample(example === null ? undefined : example),
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return schemas;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Anchor ids for a schema page's generated sections; mirrors src/lib/openapi.ts. */
|
|
595
|
+
export function schemaAnchors(page) {
|
|
596
|
+
return [
|
|
597
|
+
...(page.schema?.children?.length ? ['properties'] : []),
|
|
598
|
+
...(page.example ? ['example'] : []),
|
|
599
|
+
];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Search-index sections for a schema page, matching schemaAnchors ids. */
|
|
603
|
+
export function schemaSearchSections(page) {
|
|
604
|
+
return [
|
|
605
|
+
{
|
|
606
|
+
heading: undefined,
|
|
607
|
+
id: undefined,
|
|
608
|
+
text: [page.name, page.description].filter(Boolean).join(' '),
|
|
609
|
+
},
|
|
610
|
+
{ heading: 'Properties', id: 'properties', text: schemaText(page.schema) },
|
|
611
|
+
].filter(section => section.text.replace(/\s+/g, ' ').trim());
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/** Renders a schema page as markdown for the .md export and llms-full.txt. */
|
|
615
|
+
export function schemaToMarkdown(page) {
|
|
616
|
+
const lines = [`## ${page.name}`, ''];
|
|
617
|
+
if (page.description) lines.push(page.description, '');
|
|
618
|
+
if (page.schema?.children?.length) {
|
|
619
|
+
lines.push(
|
|
620
|
+
'### Properties',
|
|
621
|
+
'',
|
|
622
|
+
...markdownSchemaLines(page.schema)
|
|
623
|
+
.slice(1)
|
|
624
|
+
.map(line => line.slice(2)),
|
|
625
|
+
'',
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
if (page.example) lines.push('### Example', '', '```json', page.example, '```', '');
|
|
629
|
+
return lines.join('\n').trim();
|
|
630
|
+
}
|
|
631
|
+
|
|
544
632
|
function yamlString(value) {
|
|
545
633
|
return JSON.stringify(String(value).split('\n')[0]);
|
|
546
634
|
}
|
|
547
635
|
|
|
636
|
+
/** The frontmatter value that binds a page to an operation. */
|
|
637
|
+
export function operationReference(operation, prefixSpec = false) {
|
|
638
|
+
const key = operation.webhook ? `webhook ${operation.path}` : operation.key;
|
|
639
|
+
return prefixSpec && operation.spec ? `${operation.spec} ${key}` : key;
|
|
640
|
+
}
|
|
641
|
+
|
|
548
642
|
/**
|
|
549
643
|
* Writes one stub .mdx page per operation, skipping files that already exist
|
|
550
644
|
* so authors can customize titles or add prose above the generated reference.
|
|
551
645
|
*/
|
|
552
|
-
export async function generateOpenApiStubs({
|
|
553
|
-
|
|
554
|
-
|
|
646
|
+
export async function generateOpenApiStubs({
|
|
647
|
+
root,
|
|
648
|
+
contentDir,
|
|
649
|
+
directory,
|
|
650
|
+
operations,
|
|
651
|
+
prefixSpec = false,
|
|
652
|
+
}) {
|
|
555
653
|
const created = [];
|
|
556
654
|
|
|
557
655
|
for (const operation of operations) {
|
|
656
|
+
const target = path.join(
|
|
657
|
+
path.resolve(root),
|
|
658
|
+
contentDir,
|
|
659
|
+
operation.directory || directory || DEFAULT_API_DIRECTORY,
|
|
660
|
+
);
|
|
661
|
+
await fs.mkdir(target, { recursive: true });
|
|
558
662
|
const filePath = path.join(target, `${operation.id}.mdx`);
|
|
559
663
|
|
|
560
664
|
try {
|
|
@@ -568,13 +672,13 @@ export async function generateOpenApiStubs({ root, contentDir, directory, operat
|
|
|
568
672
|
'---',
|
|
569
673
|
`title: ${yamlString(operation.summary || `${operation.method} ${operation.path}`)}`,
|
|
570
674
|
...(operation.description ? [`description: ${yamlString(operation.description)}`] : []),
|
|
571
|
-
`openapi: ${operation
|
|
675
|
+
`openapi: ${operationReference(operation, prefixSpec)}`,
|
|
572
676
|
'---',
|
|
573
677
|
'',
|
|
574
678
|
].join('\n');
|
|
575
679
|
|
|
576
680
|
await fs.writeFile(filePath, frontmatter);
|
|
577
|
-
created.push(`${directory}/${operation.id}`);
|
|
681
|
+
created.push(operation.pageRef || `${directory}/${operation.id}`);
|
|
578
682
|
}
|
|
579
683
|
|
|
580
684
|
return created;
|
|
@@ -601,6 +705,15 @@ export function resolveApiDirectory(api) {
|
|
|
601
705
|
return directory;
|
|
602
706
|
}
|
|
603
707
|
|
|
708
|
+
/** Where a scheme's credential travels; mirrors securityLocation in src/lib/openapi.ts. */
|
|
709
|
+
export function securityLocation(scheme) {
|
|
710
|
+
return scheme.type === 'apiKey' && scheme.in ? scheme.in : 'header';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function securityForLocation(operation, location) {
|
|
714
|
+
return operation.security.filter(scheme => securityLocation(scheme) === location);
|
|
715
|
+
}
|
|
716
|
+
|
|
604
717
|
/** True when the operation renders a Parameters section (incl. auth). */
|
|
605
718
|
export function hasOperationParameters(operation) {
|
|
606
719
|
const { query, path: pathParams, header, cookie } = operation.parameters;
|
|
@@ -619,7 +732,7 @@ function operationParameterSections(operation) {
|
|
|
619
732
|
].filter(
|
|
620
733
|
({ location }) =>
|
|
621
734
|
operation.parameters[location].length > 0 ||
|
|
622
|
-
(location
|
|
735
|
+
securityForLocation(operation, location).length > 0,
|
|
623
736
|
);
|
|
624
737
|
}
|
|
625
738
|
|
|
@@ -627,10 +740,11 @@ function operationParameterSections(operation) {
|
|
|
627
740
|
* Anchor ids for the generated sections, in render order. Mirrors
|
|
628
741
|
* operationSections in src/lib/openapi.ts (asserted by tests/openapi.test.mjs).
|
|
629
742
|
*/
|
|
630
|
-
export function operationAnchors(operation) {
|
|
743
|
+
export function operationAnchors(operation, { playground = false } = {}) {
|
|
631
744
|
return [
|
|
745
|
+
...(playground && !operation.webhook ? ['try-it'] : []),
|
|
632
746
|
...operationParameterSections(operation).map(section => section.id),
|
|
633
|
-
...(operation.requestBody ? ['request-body'] : []),
|
|
747
|
+
...(operation.requestBody ? [operation.webhook ? 'payload' : 'request-body'] : []),
|
|
634
748
|
...(operation.responses.length ? ['responses'] : []),
|
|
635
749
|
...(operation.samples.length ? ['code-samples'] : []),
|
|
636
750
|
];
|
|
@@ -643,11 +757,41 @@ function schemaText(node) {
|
|
|
643
757
|
.join(' ');
|
|
644
758
|
}
|
|
645
759
|
|
|
646
|
-
/**
|
|
760
|
+
/**
|
|
761
|
+
* Whether an endpoint page renders the "Try it" panel. Mirrors
|
|
762
|
+
* resolvePlaygroundDisplay in src/lib/openapi.ts: the page's `playground`
|
|
763
|
+
* frontmatter wins over `api.playground.display`, and only "interactive"
|
|
764
|
+
* (the default) renders the panel.
|
|
765
|
+
*/
|
|
766
|
+
export function hasPlayground(api, frontmatterValue) {
|
|
767
|
+
const override = String(frontmatterValue ?? '').trim();
|
|
768
|
+
if (['interactive', 'simple', 'none'].includes(override)) return override === 'interactive';
|
|
769
|
+
const display = api?.playground?.display;
|
|
770
|
+
return display !== 'simple' && display !== 'none';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Normalizes an openapi frontmatter value into the operation lookup key:
|
|
775
|
+
* "get /users" -> "GET /users", "webhook userCreated" -> "WEBHOOK userCreated",
|
|
776
|
+
* and "users.yaml GET /users" -> "users.yaml GET /users" (a spec-qualified key
|
|
777
|
+
* for multi-spec sites). Returns undefined for blank values.
|
|
778
|
+
*/
|
|
647
779
|
export function normalizeOperationKey(value) {
|
|
648
780
|
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
649
|
-
const [
|
|
650
|
-
|
|
781
|
+
const [first, ...rest] = value.trim().split(/\s+/);
|
|
782
|
+
if (!rest.length) return undefined;
|
|
783
|
+
const upper = first.toUpperCase();
|
|
784
|
+
if (upper === 'WEBHOOK' || HTTP_METHODS.has(upper)) {
|
|
785
|
+
return `${upper} ${rest.join(' ')}`;
|
|
786
|
+
}
|
|
787
|
+
const inner = normalizeOperationKey(rest.join(' '));
|
|
788
|
+
return inner ? `${first} ${inner}` : undefined;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Normalizes an openapi-schema frontmatter value: "User" or "users.yaml User". */
|
|
792
|
+
export function normalizeSchemaKey(value) {
|
|
793
|
+
if (typeof value !== 'string' || !value.trim()) return undefined;
|
|
794
|
+
return value.trim().split(/\s+/).join(' ');
|
|
651
795
|
}
|
|
652
796
|
|
|
653
797
|
/** Search-index sections for an operation, matching operationAnchors ids. */
|
|
@@ -665,14 +809,16 @@ export function operationSearchSections(operation) {
|
|
|
665
809
|
id,
|
|
666
810
|
text: [
|
|
667
811
|
...operation.parameters[location].map(schemaText),
|
|
668
|
-
...(location
|
|
669
|
-
|
|
670
|
-
|
|
812
|
+
...securityForLocation(operation, location).flatMap(scheme => [
|
|
813
|
+
scheme.type === 'apiKey' ? scheme.paramName || scheme.name : 'Authorization',
|
|
814
|
+
'Authentication credentials',
|
|
815
|
+
scheme.label,
|
|
816
|
+
]),
|
|
671
817
|
].join(' '),
|
|
672
818
|
})),
|
|
673
819
|
{
|
|
674
|
-
heading: 'Request body',
|
|
675
|
-
id: 'request-body',
|
|
820
|
+
heading: operation.webhook ? 'Payload' : 'Request body',
|
|
821
|
+
id: operation.webhook ? 'payload' : 'request-body',
|
|
676
822
|
text: schemaText(operation.requestBody?.schema),
|
|
677
823
|
},
|
|
678
824
|
{
|