@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.
- package/bin/shiso.mjs +20 -1
- package/dist/chunks/App.js +363 -29
- package/dist/chunks/docs.js +33 -41
- package/dist/entry-client.js +1 -1
- package/dist/entry-server.js +30 -2
- package/docs.schema.json +90 -0
- package/mdx.config.ts +13 -97
- package/package.json +5 -4
- package/scripts/build-runtime.mjs +1 -0
- package/scripts/check-content.mjs +358 -0
- package/scripts/expand-navigation-globs.mjs +208 -0
- package/scripts/expand-openapi-navigation.mjs +94 -0
- package/scripts/generate-openapi.mjs +117 -0
- package/scripts/generate-search-index.mjs +31 -1
- package/scripts/lib/openapi.mjs +653 -0
- package/scripts/load-docs-config.mjs +52 -6
- package/scripts/load-shiso-config.mjs +45 -3
- package/scripts/prerender.mjs +83 -3
- package/scripts/vite-docs-config.mjs +27 -6
- package/src/App.tsx +0 -1
- package/src/components/CodeBlock.tsx +73 -9
- package/src/components/DocContent.tsx +24 -3
- package/src/components/Docs.tsx +6 -1
- package/src/components/OpenApiOperation.tsx +197 -0
- package/src/components/SideNav.tsx +8 -2
- package/src/components/docs/Card.tsx +1 -1
- package/src/components/docs/CodeGroup.tsx +6 -2
- package/src/components/docs/Expandable.tsx +1 -1
- package/src/components/docs/PropertiesTable.tsx +12 -25
- package/src/components/docs/styles.ts +7 -4
- package/src/entry-server.tsx +50 -0
- package/src/lib/code-blocks.ts +18 -0
- package/src/lib/code-meta.ts +87 -0
- package/src/lib/docs-config.ts +4 -1
- package/src/lib/openapi.generated.ts +4 -0
- package/src/lib/openapi.ts +63 -0
- package/src/lib/rehype-shiki.ts +196 -0
- package/src/lib/site-model.ts +2 -0
- package/src/lib/types.ts +116 -2
- package/src/styles/global.css +75 -76
- package/types/config.d.ts +11 -4
- package/vite.config.ts +49 -3
- 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
|
|
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
|
|