@umami/shiso 1.10.0 → 1.12.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 (43) hide show
  1. package/bin/shiso.mjs +20 -1
  2. package/dist/chunks/App.js +363 -29
  3. package/dist/chunks/docs.js +33 -41
  4. package/dist/entry-client.js +1 -1
  5. package/dist/entry-server.js +30 -2
  6. package/docs.schema.json +90 -0
  7. package/mdx.config.ts +13 -97
  8. package/package.json +5 -4
  9. package/scripts/build-runtime.mjs +1 -0
  10. package/scripts/check-content.mjs +358 -0
  11. package/scripts/expand-navigation-globs.mjs +208 -0
  12. package/scripts/expand-openapi-navigation.mjs +94 -0
  13. package/scripts/generate-openapi.mjs +117 -0
  14. package/scripts/generate-search-index.mjs +31 -1
  15. package/scripts/lib/openapi.mjs +653 -0
  16. package/scripts/load-docs-config.mjs +52 -6
  17. package/scripts/load-shiso-config.mjs +45 -3
  18. package/scripts/prerender.mjs +83 -3
  19. package/scripts/vite-docs-config.mjs +27 -6
  20. package/src/App.tsx +0 -1
  21. package/src/components/CodeBlock.tsx +73 -9
  22. package/src/components/DocContent.tsx +24 -3
  23. package/src/components/Docs.tsx +6 -1
  24. package/src/components/OpenApiOperation.tsx +197 -0
  25. package/src/components/SideNav.tsx +8 -2
  26. package/src/components/docs/Card.tsx +1 -1
  27. package/src/components/docs/CodeGroup.tsx +6 -2
  28. package/src/components/docs/Expandable.tsx +1 -1
  29. package/src/components/docs/PropertiesTable.tsx +12 -25
  30. package/src/components/docs/styles.ts +7 -4
  31. package/src/entry-server.tsx +50 -0
  32. package/src/lib/code-blocks.ts +18 -0
  33. package/src/lib/code-meta.ts +87 -0
  34. package/src/lib/docs-config.ts +4 -1
  35. package/src/lib/openapi.generated.ts +4 -0
  36. package/src/lib/openapi.ts +63 -0
  37. package/src/lib/rehype-shiki.ts +196 -0
  38. package/src/lib/site-model.ts +2 -0
  39. package/src/lib/types.ts +116 -2
  40. package/src/styles/global.css +75 -76
  41. package/types/config.d.ts +11 -4
  42. package/vite.config.ts +49 -3
  43. package/CHANGELOG.md +0 -171
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Emits .shiso/openapi.generated.ts: every normalized operation from the
3
+ * project's OpenAPI spec, with code samples and response examples highlighted
4
+ * by Shiki at build time so no highlighter ships to the browser.
5
+ */
6
+
7
+ import fs from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { createHighlighter, hastToHtml } from 'shiki';
10
+ import { loadOpenApiSpec, normalizeOperations } from './lib/openapi.mjs';
11
+
12
+ // Keep in sync with DEFAULT_CODE_THEME in src/lib/code-blocks.ts (asserted by
13
+ // tests/openapi.test.mjs).
14
+ export const DEFAULT_OPENAPI_THEME = { light: 'github-light', dark: 'github-dark' };
15
+
16
+ const EMPTY_MODULE = `// Generated by @umami/shiso. Do not edit by hand.
17
+ import type { NormalizedOperation } from '@/lib/types';
18
+
19
+ export const OPENAPI_OPERATIONS: Record<string, NormalizedOperation> = {};
20
+ `;
21
+
22
+ let highlighterPromise;
23
+
24
+ async function getHighlighter(theme) {
25
+ if (!highlighterPromise) {
26
+ highlighterPromise = createHighlighter({ themes: [theme.light, theme.dark], langs: [] });
27
+ }
28
+ return highlighterPromise;
29
+ }
30
+
31
+ async function highlight(highlighter, source, lang, theme) {
32
+ if (!highlighter.getLoadedLanguages().includes(lang)) {
33
+ await highlighter.loadLanguage(lang);
34
+ }
35
+
36
+ const hast = highlighter.codeToHast(source, {
37
+ lang,
38
+ themes: theme,
39
+ defaultColor: false,
40
+ });
41
+ const code = hast.children[0]?.children?.[0];
42
+ const children = code?.children || [];
43
+ const lineCount = children.filter(node => node.tagName === 'span').length;
44
+
45
+ return { html: hastToHtml({ type: 'root', children }), lineCount };
46
+ }
47
+
48
+ async function highlightOperation(operation, highlighter, theme) {
49
+ const samples = [];
50
+ for (const sample of operation.samples) {
51
+ samples.push({
52
+ ...sample,
53
+ ...(await highlight(highlighter, sample.source, sample.language, theme)),
54
+ });
55
+ }
56
+
57
+ const responses = [];
58
+ for (const response of operation.responses) {
59
+ responses.push(
60
+ response.example
61
+ ? {
62
+ ...response,
63
+ exampleHtml: (await highlight(highlighter, response.example, 'json', theme)).html,
64
+ }
65
+ : response,
66
+ );
67
+ }
68
+
69
+ const requestBody =
70
+ operation.requestBody?.example !== undefined
71
+ ? {
72
+ ...operation.requestBody,
73
+ exampleHtml: (await highlight(highlighter, operation.requestBody.example, 'json', theme))
74
+ .html,
75
+ }
76
+ : operation.requestBody;
77
+
78
+ return { ...operation, samples, responses, requestBody };
79
+ }
80
+
81
+ async function writeIfChanged(output, contents) {
82
+ const existing = await fs.readFile(output, 'utf8').catch(() => undefined);
83
+ if (existing === contents) return;
84
+ await fs.mkdir(path.dirname(output), { recursive: true });
85
+ await fs.writeFile(output, contents);
86
+ }
87
+
88
+ /** Generates the openapi data module for the project, or an empty fallback. */
89
+ export async function generateOpenApiModule({
90
+ root,
91
+ config,
92
+ theme = DEFAULT_OPENAPI_THEME,
93
+ output,
94
+ }) {
95
+ if (!config?.api?.spec) {
96
+ await writeIfChanged(output, EMPTY_MODULE);
97
+ return { operations: 0 };
98
+ }
99
+
100
+ const { spec } = await loadOpenApiSpec({ root, specPath: config.api.spec });
101
+ const operations = normalizeOperations(spec);
102
+ const highlighter = await getHighlighter(theme);
103
+ const record = {};
104
+
105
+ for (const operation of operations) {
106
+ record[operation.key] = await highlightOperation(operation, highlighter, theme);
107
+ }
108
+
109
+ const contents = `// Generated by @umami/shiso. Do not edit by hand.
110
+ import type { NormalizedOperation } from '@/lib/types';
111
+
112
+ export const OPENAPI_OPERATIONS: Record<string, NormalizedOperation> = ${JSON.stringify(record, null, 2)};
113
+ `;
114
+
115
+ await writeIfChanged(output, contents);
116
+ return { operations: operations.length };
117
+ }
@@ -22,6 +22,12 @@ import remarkMdx from 'remark-mdx';
22
22
  import remarkParse from 'remark-parse';
23
23
  import { unified } from 'unified';
24
24
  import { headingText } from './lib/mdast.mjs';
25
+ import {
26
+ loadOpenApiSpec,
27
+ normalizeOperationKey,
28
+ normalizeOperations,
29
+ operationSearchSections,
30
+ } from './lib/openapi.mjs';
25
31
  import { createSlugger, slugifyId } from './lib/slug.mjs';
26
32
  import { loadDocsConfig } from './load-docs-config.mjs';
27
33
  import { loadShisoConfig } from './load-shiso-config.mjs';
@@ -141,7 +147,13 @@ function collectVisiblePages(container, pages = [], hidden = false) {
141
147
  return pages;
142
148
  }
143
149
 
144
- /** Frontmatter is YAML, but search only needs the title line. */
150
+ /** Frontmatter is YAML, but search only needs single lines from it. */
151
+ function frontmatterOpenApi(tree) {
152
+ const yaml = tree.children?.find(node => node.type === 'yaml');
153
+ const match = yaml?.value?.match(/^openapi:\s*(.+)$/m);
154
+ return match ? match[1].trim() : undefined;
155
+ }
156
+
145
157
  function frontmatterTitle(tree) {
146
158
  const yaml = tree.children?.find(node => node.type === 'yaml');
147
159
  const match = yaml?.value?.match(/^title:\s*(.+)$/m);
@@ -211,6 +223,16 @@ export async function generateSearchIndex({
211
223
  const seen = new Set();
212
224
  const records = [];
213
225
 
226
+ // Pages bound to an API operation get synthesized sections from the spec, so
227
+ // parameters and responses are searchable even though they render from data.
228
+ let operationsByKey;
229
+ if (docsJson.api?.spec) {
230
+ const { spec } = await loadOpenApiSpec({ root, specPath: docsJson.api.spec });
231
+ operationsByKey = new Map(
232
+ normalizeOperations(spec).map(operation => [operation.key, operation]),
233
+ );
234
+ }
235
+
214
236
  for (const scope of collectScopes(docsJson.navigation || {})) {
215
237
  // Single-scope sites omit scope fields so their index stays unchanged.
216
238
  const scopeFields =
@@ -249,6 +271,14 @@ export async function generateSearchIndex({
249
271
  for (const { heading, id, text } of collectSections(tree)) {
250
272
  records.push({ url, page, heading, id, text, ...scopeFields });
251
273
  }
274
+
275
+ const operationKey = normalizeOperationKey(frontmatterOpenApi(tree));
276
+ const operation = operationKey ? operationsByKey?.get(operationKey) : undefined;
277
+ if (operation) {
278
+ for (const { heading, id, text } of operationSearchSections(operation)) {
279
+ records.push({ url, page, heading, id, text, ...scopeFields });
280
+ }
281
+ }
252
282
  }
253
283
  }
254
284