radiant-docs 0.1.58 → 0.1.60

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 (57) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +24 -2
  3. package/template/package-lock.json +216 -513
  4. package/template/package.json +13 -3
  5. package/template/scripts/generate-og-images.mjs +338 -6
  6. package/template/scripts/generate-og-metadata.mjs +29 -0
  7. package/template/src/components/Footer.astro +1 -1
  8. package/template/src/components/Header.astro +6 -13
  9. package/template/src/components/MdxPage.astro +3 -1
  10. package/template/src/components/OpenApiPage.astro +26 -832
  11. package/template/src/components/Sidebar.astro +1 -1
  12. package/template/src/components/SidebarGroup.astro +1 -1
  13. package/template/src/components/ThemeSwitcher.astro +5 -15
  14. package/template/src/components/chat/AssistantEmbedPanel.tsx +1 -1
  15. package/template/src/components/chat/AssistantEmbedPanelPage.astro +15 -2
  16. package/template/src/components/endpoint/PlaygroundButton.astro +1 -1
  17. package/template/src/components/endpoint/PlaygroundField.astro +1 -1
  18. package/template/src/components/endpoint/PlaygroundForm.astro +9 -5
  19. package/template/src/components/endpoint/ResponseFields.astro +3 -3
  20. package/template/src/components/ui/Field.astro +4 -4
  21. package/template/src/components/user/Callout.astro +2 -2
  22. package/template/src/components/user/Card.astro +2 -2
  23. package/template/src/components/user/Step.astro +3 -1
  24. package/template/src/layouts/Layout.astro +21 -46
  25. package/template/src/lib/ai-artifacts.ts +792 -0
  26. package/template/src/lib/font-css.ts +376 -0
  27. package/template/src/lib/mdx/remark-resolve-internal-links.ts +22 -8
  28. package/template/src/lib/oas.ts +5 -1
  29. package/template/src/lib/openapi/operation-doc.ts +1150 -0
  30. package/template/src/lib/page-description.ts +20 -0
  31. package/template/src/lib/routes.ts +73 -18
  32. package/template/src/pages/-/fonts/[...font].ts +50 -0
  33. package/template/src/pages/404.astro +2 -2
  34. package/template/src/pages/[...slug]/index.md.ts +35 -0
  35. package/template/src/pages/[...spec].json.ts +33 -0
  36. package/template/src/pages/[...spec].yaml.ts +33 -0
  37. package/template/src/pages/[...spec].yml.ts +33 -0
  38. package/template/src/pages/index.md.ts +17 -0
  39. package/template/src/pages/llms-full.txt.ts +11 -0
  40. package/template/src/pages/llms.txt.ts +11 -0
  41. package/template/src/styles/global.css +32 -7
  42. package/template/src/assets/fonts/geist-mono/cyrillic.woff2 +0 -0
  43. package/template/src/assets/fonts/geist-mono/latin-ext.woff2 +0 -0
  44. package/template/src/assets/fonts/geist-mono/latin.woff2 +0 -0
  45. package/template/src/assets/fonts/google-sans-flex/canadian-aboriginal.woff2 +0 -0
  46. package/template/src/assets/fonts/google-sans-flex/cherokee.woff2 +0 -0
  47. package/template/src/assets/fonts/google-sans-flex/latin-ext.woff2 +0 -0
  48. package/template/src/assets/fonts/google-sans-flex/latin.woff2 +0 -0
  49. package/template/src/assets/fonts/google-sans-flex/math.woff2 +0 -0
  50. package/template/src/assets/fonts/google-sans-flex/nushu.woff2 +0 -0
  51. package/template/src/assets/fonts/google-sans-flex/symbols.woff2 +0 -0
  52. package/template/src/assets/fonts/google-sans-flex/syriac.woff2 +0 -0
  53. package/template/src/assets/fonts/google-sans-flex/tifinagh.woff2 +0 -0
  54. package/template/src/assets/fonts/google-sans-flex/vietnamese.woff2 +0 -0
  55. package/template/src/styles/geist-mono.css +0 -33
  56. package/template/src/styles/google-sans-flex.css +0 -143
  57. package/template/src/styles/vaul.css +0 -255
@@ -0,0 +1,376 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import { resolveStaticAssetUrl } from "./static-asset-url";
6
+ import {
7
+ DEFAULT_CODE_FONT_PRESET,
8
+ DEFAULT_TEXT_FONT_PRESET,
9
+ FONT_PRESET_OPTIONS,
10
+ type DocsConfig,
11
+ type FontPresetOption,
12
+ } from "./validation";
13
+
14
+ const require = createRequire(import.meta.url);
15
+
16
+ const FONT_ROUTE_PREFIX = "/-/fonts";
17
+ const FONT_CONTENT_TYPE = "font/woff2";
18
+ const FONT_DISPLAY = "block";
19
+
20
+ type FontPresetId = FontPresetOption;
21
+
22
+ type FontSourceFile = {
23
+ fileName: string;
24
+ preload?: boolean;
25
+ };
26
+
27
+ type FontsourceConfig = {
28
+ packageName: string;
29
+ routeDir: string;
30
+ cssFiles: string[];
31
+ files: FontSourceFile[];
32
+ };
33
+
34
+ type FontPreset = {
35
+ stack: string;
36
+ fontsource?: FontsourceConfig;
37
+ };
38
+
39
+ export type FontPreload = {
40
+ href: string;
41
+ type: "font/woff2";
42
+ };
43
+
44
+ export type DocsFontFile = {
45
+ routePath: string;
46
+ sourcePath: string;
47
+ contentType: typeof FONT_CONTENT_TYPE;
48
+ };
49
+
50
+ const fontCssCache = new Map<FontPresetId, string>();
51
+ const fontRoutePathCache = new Map<string, string>();
52
+
53
+ const LATIN_UNICODE_FILES = ["latin-ext", "latin"] as const;
54
+ const FONT_STYLES = ["normal", "italic"] as const;
55
+
56
+ function getFontsourceFiles(fontId: string): FontSourceFile[] {
57
+ return FONT_STYLES.flatMap((style) =>
58
+ LATIN_UNICODE_FILES.map((subset) => ({
59
+ fileName: `${fontId}-${subset}-wght-${style}.woff2`,
60
+ preload: subset === "latin" && style === "normal",
61
+ })),
62
+ );
63
+ }
64
+
65
+ const FONT_PRESETS: Record<FontPresetId, FontPreset> = {
66
+ "google-sans": {
67
+ stack: '"Google Sans Variable", ui-sans-serif, system-ui, sans-serif',
68
+ fontsource: {
69
+ packageName: "@fontsource-variable/google-sans",
70
+ routeDir: "google-sans",
71
+ cssFiles: ["wght.css", "wght-italic.css"],
72
+ files: getFontsourceFiles("google-sans"),
73
+ },
74
+ },
75
+ inter: {
76
+ stack: '"Inter Variable", ui-sans-serif, system-ui, sans-serif',
77
+ fontsource: {
78
+ packageName: "@fontsource-variable/inter",
79
+ routeDir: "inter",
80
+ cssFiles: ["wght.css", "wght-italic.css"],
81
+ files: getFontsourceFiles("inter"),
82
+ },
83
+ },
84
+ "inter-tight": {
85
+ stack: '"Inter Tight Variable", ui-sans-serif, system-ui, sans-serif',
86
+ fontsource: {
87
+ packageName: "@fontsource-variable/inter-tight",
88
+ routeDir: "inter-tight",
89
+ cssFiles: ["wght.css", "wght-italic.css"],
90
+ files: getFontsourceFiles("inter-tight"),
91
+ },
92
+ },
93
+ geist: {
94
+ stack: '"Geist Variable", ui-sans-serif, system-ui, sans-serif',
95
+ fontsource: {
96
+ packageName: "@fontsource-variable/geist",
97
+ routeDir: "geist",
98
+ cssFiles: ["wght.css", "wght-italic.css"],
99
+ files: getFontsourceFiles("geist"),
100
+ },
101
+ },
102
+ "source-sans-3": {
103
+ stack: '"Source Sans 3 Variable", ui-sans-serif, system-ui, sans-serif',
104
+ fontsource: {
105
+ packageName: "@fontsource-variable/source-sans-3",
106
+ routeDir: "source-sans-3",
107
+ cssFiles: ["wght.css", "wght-italic.css"],
108
+ files: getFontsourceFiles("source-sans-3"),
109
+ },
110
+ },
111
+ "source-serif-4": {
112
+ stack:
113
+ '"Source Serif 4 Variable", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
114
+ fontsource: {
115
+ packageName: "@fontsource-variable/source-serif-4",
116
+ routeDir: "source-serif-4",
117
+ cssFiles: ["wght.css", "wght-italic.css"],
118
+ files: getFontsourceFiles("source-serif-4"),
119
+ },
120
+ },
121
+ "public-sans": {
122
+ stack: '"Public Sans Variable", ui-sans-serif, system-ui, sans-serif',
123
+ fontsource: {
124
+ packageName: "@fontsource-variable/public-sans",
125
+ routeDir: "public-sans",
126
+ cssFiles: ["wght.css", "wght-italic.css"],
127
+ files: getFontsourceFiles("public-sans"),
128
+ },
129
+ },
130
+ "geist-mono": {
131
+ stack:
132
+ '"Geist Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
133
+ fontsource: {
134
+ packageName: "@fontsource-variable/geist-mono",
135
+ routeDir: "geist-mono",
136
+ cssFiles: ["wght.css", "wght-italic.css"],
137
+ files: getFontsourceFiles("geist-mono"),
138
+ },
139
+ },
140
+ "jetbrains-mono": {
141
+ stack:
142
+ '"JetBrains Mono Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
143
+ fontsource: {
144
+ packageName: "@fontsource-variable/jetbrains-mono",
145
+ routeDir: "jetbrains-mono",
146
+ cssFiles: ["wght.css", "wght-italic.css"],
147
+ files: getFontsourceFiles("jetbrains-mono"),
148
+ },
149
+ },
150
+ "google-sans-code": {
151
+ stack:
152
+ '"Google Sans Code Variable", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
153
+ fontsource: {
154
+ packageName: "@fontsource-variable/google-sans-code",
155
+ routeDir: "google-sans-code",
156
+ cssFiles: ["wght.css", "wght-italic.css"],
157
+ files: getFontsourceFiles("google-sans-code"),
158
+ },
159
+ },
160
+ "system-sans": {
161
+ stack: 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
162
+ },
163
+ "system-serif": {
164
+ stack: 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
165
+ },
166
+ "system-mono": {
167
+ stack:
168
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
169
+ },
170
+ };
171
+
172
+ type FontThemeConfig = {
173
+ text?: unknown;
174
+ heading?: unknown;
175
+ code?: unknown;
176
+ };
177
+
178
+ const FONT_PRESET_ID_SET = new Set<FontPresetId>(FONT_PRESET_OPTIONS);
179
+
180
+ function getConfiguredFonts(theme: DocsConfig["theme"]): FontThemeConfig {
181
+ return theme?.fonts ?? {};
182
+ }
183
+
184
+ function normalizeFontPresetId(value: unknown): FontPresetId | null {
185
+ if (typeof value !== "string") return null;
186
+
187
+ const normalized = value.trim().toLowerCase();
188
+ return FONT_PRESET_ID_SET.has(normalized as FontPresetId)
189
+ ? (normalized as FontPresetId)
190
+ : null;
191
+ }
192
+
193
+ function resolveFontPresetIds(theme: DocsConfig["theme"]): {
194
+ text: FontPresetId;
195
+ heading: FontPresetId;
196
+ code: FontPresetId;
197
+ } {
198
+ const fonts = getConfiguredFonts(theme);
199
+ const text = normalizeFontPresetId(fonts.text) ?? DEFAULT_TEXT_FONT_PRESET;
200
+ return {
201
+ text,
202
+ heading: normalizeFontPresetId(fonts.heading) ?? text,
203
+ code: normalizeFontPresetId(fonts.code) ?? DEFAULT_CODE_FONT_PRESET,
204
+ };
205
+ }
206
+
207
+ function getFontsourceCssPath(
208
+ fontsource: FontsourceConfig,
209
+ cssFile: string,
210
+ ): string {
211
+ return require.resolve(`${fontsource.packageName}/${cssFile}`);
212
+ }
213
+
214
+ function getFontsourceFilePath(
215
+ fontsource: FontsourceConfig,
216
+ fileName: string,
217
+ ): string {
218
+ return require.resolve(`${fontsource.packageName}/files/${fileName}`);
219
+ }
220
+
221
+ function getFontsourceCssUrlPattern(): RegExp {
222
+ return /url\((["']?)\.\/files\/([^"')]+)\1\)/g;
223
+ }
224
+
225
+ function getFontFaceBlockPattern(): RegExp {
226
+ return /(?:\/\*[\s\S]*?\*\/\s*)?@font-face\s*\{[\s\S]*?\}\s*/g;
227
+ }
228
+
229
+ function applyFontDisplay(block: string): string {
230
+ return block.replace(/font-display:\s*[^;]+;/, `font-display: ${FONT_DISPLAY};`);
231
+ }
232
+
233
+ function getFontRoutePath(
234
+ fontsource: FontsourceConfig,
235
+ fileName: string,
236
+ ): string {
237
+ const cacheKey = `${fontsource.packageName}/${fileName}`;
238
+ const cachedRoutePath = fontRoutePathCache.get(cacheKey);
239
+ if (cachedRoutePath) return cachedRoutePath;
240
+
241
+ const sourcePath = getFontsourceFilePath(fontsource, fileName);
242
+ const hash = crypto
243
+ .createHash("sha256")
244
+ .update(fs.readFileSync(sourcePath))
245
+ .digest("hex")
246
+ .slice(0, 12);
247
+ const extension = path.extname(fileName);
248
+ const basename = path.basename(fileName, extension);
249
+ const routePath = `${fontsource.routeDir}/${basename}.${hash}${extension}`;
250
+
251
+ fontRoutePathCache.set(cacheKey, routePath);
252
+ return routePath;
253
+ }
254
+
255
+ function getFontPublicUrl(
256
+ fontsource: FontsourceConfig,
257
+ fileName: string,
258
+ ): string {
259
+ return resolveStaticAssetUrl(
260
+ `${FONT_ROUTE_PREFIX}/${getFontRoutePath(fontsource, fileName)}`,
261
+ );
262
+ }
263
+
264
+ function getFontsourceCss(presetId: FontPresetId): string {
265
+ const cachedCss = fontCssCache.get(presetId);
266
+ if (cachedCss !== undefined) return cachedCss;
267
+
268
+ const fontsource = FONT_PRESETS[presetId].fontsource;
269
+ if (!fontsource) return "";
270
+
271
+ const includedFiles = new Set(
272
+ fontsource.files.map((fontFile) => fontFile.fileName),
273
+ );
274
+ const matchedFiles = new Set<string>();
275
+ const resolvedCss = fontsource.cssFiles
276
+ .flatMap((cssFile) => {
277
+ const css = fs.readFileSync(getFontsourceCssPath(fontsource, cssFile), {
278
+ encoding: "utf8",
279
+ });
280
+
281
+ return Array.from(css.matchAll(getFontFaceBlockPattern()), ([block]) => {
282
+ const urlMatch = getFontsourceCssUrlPattern().exec(block);
283
+ const fileName = urlMatch?.[2];
284
+ if (!fileName || !includedFiles.has(fileName)) return "";
285
+
286
+ matchedFiles.add(fileName);
287
+ return applyFontDisplay(block).replace(
288
+ getFontsourceCssUrlPattern(),
289
+ (_match, _quote, matchedFileName) =>
290
+ `url("${getFontPublicUrl(fontsource, matchedFileName)}")`,
291
+ );
292
+ });
293
+ })
294
+ .filter(Boolean)
295
+ .join("\n");
296
+
297
+ const missingFiles = Array.from(includedFiles).filter(
298
+ (fileName) => !matchedFiles.has(fileName),
299
+ );
300
+ if (missingFiles.length > 0) {
301
+ throw new Error(
302
+ `Font preset "${presetId}" is missing @font-face blocks for: ${missingFiles.join(
303
+ ", ",
304
+ )}.`,
305
+ );
306
+ }
307
+
308
+ fontCssCache.set(presetId, resolvedCss);
309
+ return resolvedCss;
310
+ }
311
+
312
+ function getSelectedFontPresetIds(theme: DocsConfig["theme"]): FontPresetId[] {
313
+ const fontPresetIds = resolveFontPresetIds(theme);
314
+ return [...new Set(Object.values(fontPresetIds))];
315
+ }
316
+
317
+ export function getDocsFontCss(theme: DocsConfig["theme"]): string {
318
+ const fontPresetIds = resolveFontPresetIds(theme);
319
+ const fontFaceCss = getSelectedFontPresetIds(theme).map((presetId) =>
320
+ getFontsourceCss(presetId),
321
+ );
322
+
323
+ return [
324
+ ...fontFaceCss.filter(Boolean),
325
+ ":root {",
326
+ ` --rd-font-text: ${FONT_PRESETS[fontPresetIds.text].stack};`,
327
+ ` --rd-font-heading: ${FONT_PRESETS[fontPresetIds.heading].stack};`,
328
+ ` --rd-font-code: ${FONT_PRESETS[fontPresetIds.code].stack};`,
329
+ "}",
330
+ ].join("\n");
331
+ }
332
+
333
+ export function getDocsFontPreloads(
334
+ theme: DocsConfig["theme"],
335
+ ): FontPreload[] {
336
+ const preloadUrls = new Set<string>();
337
+
338
+ for (const presetId of getSelectedFontPresetIds(theme)) {
339
+ const fontsource = FONT_PRESETS[presetId].fontsource;
340
+ if (!fontsource) continue;
341
+
342
+ for (const fontFile of fontsource.files) {
343
+ if (!fontFile.preload) continue;
344
+ preloadUrls.add(getFontPublicUrl(fontsource, fontFile.fileName));
345
+ }
346
+ }
347
+
348
+ return Array.from(preloadUrls, (href) => ({
349
+ href,
350
+ type: "font/woff2" as const,
351
+ }));
352
+ }
353
+
354
+ export function getSelectedDocsFontFiles(
355
+ theme: DocsConfig["theme"],
356
+ ): DocsFontFile[] {
357
+ const fontFiles = new Map<string, DocsFontFile>();
358
+
359
+ for (const presetId of getSelectedFontPresetIds(theme)) {
360
+ const fontsource = FONT_PRESETS[presetId].fontsource;
361
+ if (!fontsource) continue;
362
+
363
+ for (const fontFile of fontsource.files) {
364
+ const routePath = getFontRoutePath(fontsource, fontFile.fileName);
365
+ fontFiles.set(routePath, {
366
+ routePath,
367
+ sourcePath: getFontsourceFilePath(fontsource, fontFile.fileName),
368
+ contentType: FONT_CONTENT_TYPE,
369
+ });
370
+ }
371
+ }
372
+
373
+ return Array.from(fontFiles.values()).sort((a, b) =>
374
+ a.routePath.localeCompare(b.routePath),
375
+ );
376
+ }
@@ -8,7 +8,6 @@ import {
8
8
  loadOpenApiSpec,
9
9
  resolveDocsHref,
10
10
  type DocsConfig,
11
- type HiddenPageRoute,
12
11
  type NavGroup,
13
12
  type NavMenuItem,
14
13
  type NavOpenApi,
@@ -28,6 +27,15 @@ import {
28
27
 
29
28
  type MdxNavItem = string | NavPage | NavGroup | NavOpenApiPage;
30
29
 
30
+ type AuxiliaryPageRef = {
31
+ filePath: string;
32
+ href: string;
33
+ };
34
+
35
+ type DocsConfigWithAuxiliaryRefs = DocsConfig & {
36
+ auxiliaryPageRefs?: AuxiliaryPageRef[];
37
+ };
38
+
31
39
  type ResolvedRouteIndex = {
32
40
  canonicalHrefByFilePath: Map<string, string>;
33
41
  allHrefsByFilePath: Map<string, string[]>;
@@ -230,14 +238,14 @@ function addOpenApiEndpointRoute(args: {
230
238
  );
231
239
  }
232
240
 
233
- function addHiddenPageRoute(
241
+ function addAuxiliaryPageRef(
234
242
  index: ResolvedRouteIndex,
235
- route: HiddenPageRoute,
243
+ ref: AuxiliaryPageRef,
236
244
  ): void {
237
- const normalizedFilePath = normalizeDocsFilePath(route.filePath);
245
+ const normalizedFilePath = normalizeDocsFilePath(ref.filePath);
238
246
  if (!normalizedFilePath) return;
239
247
 
240
- const href = addValidRoutePath(index, prependBasePath(route.href));
248
+ const href = addValidRoutePath(index, prependBasePath(ref.href));
241
249
  if (!index.canonicalHrefByFilePath.has(normalizedFilePath)) {
242
250
  index.canonicalHrefByFilePath.set(normalizedFilePath, href);
243
251
  }
@@ -249,6 +257,12 @@ function addHiddenPageRoute(
249
257
  index.allHrefsByFilePath.set(normalizedFilePath, aliases);
250
258
  }
251
259
 
260
+ function getAuxiliaryPageRefs(
261
+ config: DocsConfigWithAuxiliaryRefs,
262
+ ): AuxiliaryPageRef[] {
263
+ return config.auxiliaryPageRefs ?? [];
264
+ }
265
+
252
266
  function shouldIncludeEndpoint(
253
267
  method: string,
254
268
  pathStr: string,
@@ -474,14 +488,14 @@ async function buildRouteIndex(
474
488
  }
475
489
 
476
490
  if (homePath) {
477
- addHiddenPageRoute(index, {
491
+ addAuxiliaryPageRef(index, {
478
492
  filePath: homePath,
479
493
  href: "/",
480
494
  });
481
495
  }
482
496
 
483
- for (const hiddenPageRoute of config.hiddenPageRoutes ?? []) {
484
- addHiddenPageRoute(index, hiddenPageRoute);
497
+ for (const auxiliaryPageRef of getAuxiliaryPageRefs(config)) {
498
+ addAuxiliaryPageRef(index, auxiliaryPageRef);
485
499
  }
486
500
 
487
501
  return index;
@@ -4,6 +4,10 @@ import { loadOpenApiSpec } from "./validation";
4
4
  // Cache for dereferenced Oas instances (key: filePathOrUrl, value: Oas instance)
5
5
  const oasInstanceCache = new Map<string, Oas>();
6
6
 
7
+ function cloneOpenApiDocument<T>(document: T): T {
8
+ return JSON.parse(JSON.stringify(document));
9
+ }
10
+
7
11
  export async function getOasInstance(filePathOrUrl: string): Promise<Oas> {
8
12
  // Check cache first
9
13
  if (oasInstanceCache.has(filePathOrUrl)) {
@@ -14,7 +18,7 @@ export async function getOasInstance(filePathOrUrl: string): Promise<Oas> {
14
18
  const openApiDoc = await loadOpenApiSpec(filePathOrUrl);
15
19
 
16
20
  // Create and dereference Oas instance
17
- const api = new Oas(openApiDoc);
21
+ const api = new Oas(cloneOpenApiDocument(openApiDoc));
18
22
  await api.dereference();
19
23
 
20
24
  // Cache the dereferenced instance