@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.
Files changed (64) hide show
  1. package/dist/chunks/App.js +1217 -196
  2. package/dist/chunks/architectureDiagram-5GKGNRK7.js +1 -1
  3. package/dist/chunks/chunk-GMAD6QVW.js +1 -1
  4. package/dist/chunks/cose-bilkent-JH36ORCC.js +1 -1
  5. package/dist/chunks/dist.js +1 -1
  6. package/dist/chunks/docs.js +843 -22
  7. package/dist/chunks/ganttDiagram-EL5Y4UJY.js +1 -1
  8. package/dist/chunks/src.js +1 -1
  9. package/dist/components.js +1 -1
  10. package/dist/entry-client.js +1 -1
  11. package/dist/entry-server.js +1 -1
  12. package/docs.schema.json +1164 -1131
  13. package/package.json +1 -2
  14. package/scripts/check-content.mjs +44 -15
  15. package/scripts/expand-openapi-navigation.mjs +47 -21
  16. package/scripts/generate-openapi.mjs +36 -11
  17. package/scripts/generate-search-index.mjs +19 -18
  18. package/scripts/lib/openapi-project.mjs +197 -0
  19. package/scripts/lib/openapi.mjs +257 -111
  20. package/scripts/lib/request-samples.mjs +323 -0
  21. package/scripts/load-docs-config.mjs +23 -15
  22. package/scripts/load-shiso-config.mjs +30 -1
  23. package/scripts/prerender.mjs +17 -14
  24. package/scripts/vite-docs-config.mjs +1 -0
  25. package/src/App.tsx +32 -22
  26. package/src/components/ApiPlayground.tsx +522 -0
  27. package/src/components/CodeBlock.tsx +3 -1
  28. package/src/components/DocContent.tsx +15 -4
  29. package/src/components/Docs.tsx +19 -2
  30. package/src/components/Footer.tsx +3 -1
  31. package/src/components/Header.tsx +4 -3
  32. package/src/components/LanguageSwitcher.tsx +38 -29
  33. package/src/components/OpenApiOperation.tsx +73 -20
  34. package/src/components/OpenApiSchema.tsx +97 -0
  35. package/src/components/PageActions.tsx +8 -7
  36. package/src/components/SideNav.tsx +2 -2
  37. package/src/components/docs/Changelog.tsx +5 -5
  38. package/src/components/docs/CodeGroup.tsx +3 -1
  39. package/src/components/docs/Mermaid.tsx +8 -6
  40. package/src/components/docs/PropertiesTable.tsx +11 -6
  41. package/src/components/docs/Tabs.tsx +3 -1
  42. package/src/components/docs/Tree.tsx +3 -1
  43. package/src/components/docs/ZoomableImage.tsx +4 -2
  44. package/src/components/ui/dialog.tsx +7 -2
  45. package/src/components/ui/sheet.tsx +5 -2
  46. package/src/lib/label-context.tsx +7 -0
  47. package/src/lib/labels.ts +37 -0
  48. package/src/lib/openapi.generated.ts +2 -1
  49. package/src/lib/openapi.ts +123 -16
  50. package/src/lib/site-config.ts +66 -2
  51. package/src/lib/site-model.ts +14 -32
  52. package/src/lib/standalone-pages.ts +15 -1
  53. package/src/lib/translations/de.json +99 -0
  54. package/src/lib/translations/en.json +99 -0
  55. package/src/lib/translations/es.json +99 -0
  56. package/src/lib/translations/fr.json +99 -0
  57. package/src/lib/translations/ja.json +99 -0
  58. package/src/lib/translations/zh-Hans.json +99 -0
  59. package/src/lib/translations/zh-Hant.json +99 -0
  60. package/src/lib/types.ts +109 -37
  61. package/types/config.d.ts +7 -1
  62. package/types/labels.d.ts +101 -0
  63. package/vite.config.ts +3 -4
  64. 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
- generateOpenApiStubs,
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 specPath;
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 loadedSpec = await loadOpenApiSpec({ root: projectRoot, specPath: working.api.spec });
253
- specPath = loadedSpec.specPath;
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 { config, projectRoot, sourcePath, sourcePaths, hasGlobs, specPath };
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({
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { createJiti } from 'jiti';
5
+ import englishLabels from '../src/lib/translations/en.json' with { type: 'json' };
5
6
 
6
7
  /**
7
8
  * Loads the optional project code config (shiso.config.ts/.mjs/.js).
@@ -16,7 +17,7 @@ import { createJiti } from 'jiti';
16
17
  export const SHISO_CONFIG_FILES = ['shiso.config.ts', 'shiso.config.mjs', 'shiso.config.js'];
17
18
 
18
19
  const STRING_KEYS = ['docsPrefix', 'contentDir', 'siteUrl', 'locale'];
19
- const KNOWN_KEYS = [...STRING_KEYS, 'mdx'];
20
+ const KNOWN_KEYS = [...STRING_KEYS, 'mdx', 'translations'];
20
21
  const MDX_KEYS = ['remarkPlugins', 'rehypePlugins'];
21
22
 
22
23
  let importGeneration = 0;
@@ -88,6 +89,31 @@ function resolveMdxConfig(value, sourcePath) {
88
89
  };
89
90
  }
90
91
 
92
+ function resolveTranslations(value, sourcePath) {
93
+ if (value === undefined) return undefined;
94
+ const invalid = message => {
95
+ throw new ShisoConfigLoadError(message, { code: 'INVALID_OPTION', sourcePath });
96
+ };
97
+ if (!isPlainObject(value)) invalid('Shiso config option "translations" must be an object.');
98
+ const result = {};
99
+ for (const [locale, labels] of Object.entries(value)) {
100
+ let canonical;
101
+ try {
102
+ canonical = Intl.getCanonicalLocales(locale)[0];
103
+ } catch {
104
+ invalid(`Invalid translations locale "${locale}". Use a BCP 47 tag such as "fr" or "pt-BR".`);
105
+ }
106
+ if (!isPlainObject(labels)) invalid(`translations.${locale} must be an object.`);
107
+ for (const [key, label] of Object.entries(labels)) {
108
+ if (!Object.hasOwn(englishLabels, key))
109
+ invalid(`Unknown UI label "translations.${locale}.${key}".`);
110
+ if (typeof label !== 'string') invalid(`translations.${locale}.${key} must be a string.`);
111
+ }
112
+ result[canonical] = { ...result[canonical], ...labels };
113
+ }
114
+ return result;
115
+ }
116
+
91
117
  /**
92
118
  * Applies defaults and normalization. Single source of truth for resolved
93
119
  * values, so runtime and build-time consumers never re-implement defaulting.
@@ -103,6 +129,9 @@ export function resolveShisoConfig(raw = {}, sourcePath = null) {
103
129
  siteUrl: raw.siteUrl?.trim().replace(/\/+$/, '') || undefined,
104
130
  locale: raw.locale?.trim() || 'en-US',
105
131
  mdx: resolveMdxConfig(raw.mdx, sourcePath),
132
+ ...(raw.translations === undefined
133
+ ? {}
134
+ : { translations: resolveTranslations(raw.translations, sourcePath) }),
106
135
  };
107
136
  }
108
137
 
@@ -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
- loadOpenApiSpec,
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 openApiByKey;
121
+ let apiProject;
126
122
  {
127
123
  const docsConfig = (await loadDocsConfig({ root, expandGlobs: false })).config;
128
124
  if (docsConfig.api?.spec) {
129
- const { spec } = await loadOpenApiSpec({ root, specPath: docsConfig.api.spec });
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 withOperationMarkdown(source) {
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
- const key = normalizeOperationKey(frontmatter.match(/^openapi:\s*(.+)$/m)?.[1]);
138
- const operation = key ? openApiByKey.get(key) : undefined;
139
- return operation ? `${source.trimEnd()}\n\n${operationToMarkdown(operation)}\n` : source;
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,
package/src/App.tsx CHANGED
@@ -3,12 +3,18 @@ import '@fontsource/jetbrains-mono/400.css';
3
3
  import '@umami/shiso/styles.css';
4
4
 
5
5
  import { MDXProvider } from '@mdx-js/react';
6
- import { Navigate, Route, Routes } from 'react-router';
6
+ import { Navigate, Route, Routes, useLocation } from 'react-router';
7
7
  import { CodeBlock } from '@/components/CodeBlock';
8
8
  import * as docsComponents from '@/components/docs/index';
9
9
  import { Layout } from '@/components/Layout';
10
10
  import { TooltipProvider } from '@/components/ui/tooltip';
11
- import { docsHomeUrl, hasRootStandalonePage, siteModel, standalonePages } from '@/lib/site-config';
11
+ import { LabelContext } from '@/lib/label-context';
12
+ import {
13
+ docsHomeUrl,
14
+ getSiteModelByPathname,
15
+ hasRootStandalonePage,
16
+ standalonePages,
17
+ } from '@/lib/site-config';
12
18
  import { DocPage } from '@/pages/DocPage';
13
19
  import { StandalonePageView } from '@/pages/StandalonePage';
14
20
 
@@ -27,27 +33,31 @@ const mdxComponents = {
27
33
  };
28
34
 
29
35
  export function App() {
36
+ const { pathname } = useLocation();
37
+ const siteModel = getSiteModelByPathname(pathname);
30
38
  return (
31
- <TooltipProvider>
32
- <MDXProvider components={mdxComponents}>
33
- <Layout site={siteModel}>
34
- <Routes>
35
- {standalonePages.map(page => (
36
- <Route
37
- key={page.path}
38
- path={page.path}
39
- element={<StandalonePageView page={page} site={siteModel} />}
40
- />
41
- ))}
42
- {/* When the default scope's landing page is the root, or a
39
+ <LabelContext.Provider value={siteModel.labels}>
40
+ <TooltipProvider>
41
+ <MDXProvider components={mdxComponents}>
42
+ <Layout site={siteModel}>
43
+ <Routes>
44
+ {standalonePages.map(page => (
45
+ <Route
46
+ key={page.path}
47
+ path={page.path}
48
+ element={<StandalonePageView page={page} site={siteModel} />}
49
+ />
50
+ ))}
51
+ {/* When the default scope's landing page is the root, or a
43
52
  standalone page owns "/", there is nothing to redirect. */}
44
- {docsHomeUrl !== '/' && !hasRootStandalonePage ? (
45
- <Route path="/" element={<Navigate to={docsHomeUrl} replace />} />
46
- ) : null}
47
- <Route path="*" element={<DocPage site={siteModel} />} />
48
- </Routes>
49
- </Layout>
50
- </MDXProvider>
51
- </TooltipProvider>
53
+ {docsHomeUrl !== '/' && !hasRootStandalonePage ? (
54
+ <Route path="/" element={<Navigate to={docsHomeUrl} replace />} />
55
+ ) : null}
56
+ <Route path="*" element={<DocPage site={siteModel} />} />
57
+ </Routes>
58
+ </Layout>
59
+ </MDXProvider>
60
+ </TooltipProvider>
61
+ </LabelContext.Provider>
52
62
  );
53
63
  }