@stainless-api/docs 0.1.0-beta.7 → 0.1.0-beta.70

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 (120) hide show
  1. package/CHANGELOG.md +554 -0
  2. package/README.md +1 -1
  3. package/eslint-suppressions.json +52 -0
  4. package/locals.d.ts +17 -0
  5. package/package.json +51 -40
  6. package/plugin/assets/languages/csharp.svg +1 -0
  7. package/plugin/buildAlgoliaIndex.ts +32 -7
  8. package/plugin/cms/server.ts +130 -58
  9. package/plugin/cms/sidebar-builder.ts +7 -26
  10. package/plugin/cms/worker.ts +83 -5
  11. package/plugin/components/MethodDescription.tsx +54 -0
  12. package/plugin/components/SDKSelect.astro +7 -87
  13. package/plugin/components/SnippetCode.tsx +53 -8
  14. package/plugin/components/search/SearchAlgolia.astro +45 -28
  15. package/plugin/components/search/SearchIsland.tsx +38 -24
  16. package/plugin/create-playground.shim.tsx +3 -0
  17. package/plugin/generateAPIReferenceLink.ts +2 -2
  18. package/plugin/globalJs/ai-dropdown-options.ts +243 -0
  19. package/plugin/globalJs/code-snippets.ts +15 -8
  20. package/plugin/globalJs/copy.ts +81 -16
  21. package/plugin/globalJs/method-descriptions.ts +33 -0
  22. package/plugin/globalJs/navigation.ts +7 -4
  23. package/plugin/helpers/generateDocsRoutes.ts +27 -0
  24. package/plugin/index.ts +178 -35
  25. package/plugin/languages.ts +5 -2
  26. package/plugin/loadPluginConfig.ts +121 -32
  27. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +1 -1
  28. package/plugin/react/Routing.tsx +208 -129
  29. package/plugin/referencePlaceholderUtils.ts +1 -1
  30. package/plugin/replaceSidebarPlaceholderMiddleware.ts +5 -1
  31. package/plugin/routes/Docs.astro +62 -89
  32. package/plugin/routes/DocsStatic.astro +1 -1
  33. package/plugin/routes/Overview.astro +10 -16
  34. package/plugin/routes/markdown.ts +9 -8
  35. package/plugin/vendor/preview.worker.docs.js +19768 -17702
  36. package/plugin/vendor/templates/go.md +1 -1
  37. package/plugin/vendor/templates/python.md +1 -1
  38. package/resolveSrcFile.ts +10 -0
  39. package/scripts/vendor_deps.ts +5 -5
  40. package/shared/getProsePages.ts +42 -0
  41. package/shared/getSharedLogger.ts +15 -0
  42. package/shared/terminalUtils.ts +3 -0
  43. package/src/content.config.ts +9 -0
  44. package/stl-docs/components/AIDropdown.tsx +63 -0
  45. package/stl-docs/components/AiChatIsland.tsx +14 -0
  46. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +10 -18
  47. package/stl-docs/components/Head.astro +16 -0
  48. package/stl-docs/components/Header.astro +6 -8
  49. package/stl-docs/components/PageFrame.astro +18 -0
  50. package/stl-docs/components/PageTitle.astro +82 -0
  51. package/stl-docs/components/TableOfContents.astro +34 -0
  52. package/stl-docs/components/ThemeProvider.astro +36 -0
  53. package/stl-docs/components/ThemeSelect.astro +84 -139
  54. package/stl-docs/components/content-panel/ContentPanel.astro +16 -25
  55. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +17 -1
  56. package/stl-docs/components/headers/StackedHeader.astro +29 -24
  57. package/stl-docs/components/icons/chat-gpt.tsx +17 -0
  58. package/stl-docs/components/icons/claude.tsx +10 -0
  59. package/stl-docs/components/icons/cursor.tsx +10 -0
  60. package/stl-docs/components/icons/gemini.tsx +19 -0
  61. package/stl-docs/components/icons/markdown.tsx +10 -0
  62. package/stl-docs/components/index.ts +1 -0
  63. package/stl-docs/components/mintlify-compat/Accordion.astro +7 -5
  64. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +7 -3
  65. package/stl-docs/components/mintlify-compat/Columns.astro +40 -42
  66. package/stl-docs/components/mintlify-compat/Frame.astro +16 -18
  67. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +1 -1
  68. package/stl-docs/components/mintlify-compat/callouts/Check.astro +1 -1
  69. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +1 -1
  70. package/stl-docs/components/mintlify-compat/callouts/Info.astro +1 -1
  71. package/stl-docs/components/mintlify-compat/callouts/Note.astro +1 -1
  72. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +1 -1
  73. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +1 -1
  74. package/stl-docs/components/mintlify-compat/card.css +33 -35
  75. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  76. package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -70
  77. package/stl-docs/components/nav-tabs/NavTabs.astro +78 -80
  78. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -8
  79. package/stl-docs/components/nav-tabs/buildNavLinks.ts +3 -2
  80. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  81. package/stl-docs/components/pagination/Pagination.astro +175 -0
  82. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  83. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  84. package/stl-docs/components/pagination/util.ts +71 -0
  85. package/stl-docs/components/scripts.ts +1 -0
  86. package/stl-docs/disableCalloutSyntax.ts +36 -0
  87. package/stl-docs/index.ts +141 -50
  88. package/stl-docs/loadStlDocsConfig.ts +45 -5
  89. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +61 -0
  90. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +39 -0
  91. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  92. package/stl-docs/proseSearchIndexing.ts +450 -0
  93. package/stl-docs/tabsMiddleware.ts +11 -3
  94. package/styles/code.css +108 -140
  95. package/styles/fonts.css +32 -17
  96. package/styles/links.css +11 -48
  97. package/styles/method-descriptions.css +36 -0
  98. package/styles/overrides.css +48 -60
  99. package/styles/page.css +92 -52
  100. package/styles/sdk_select.css +9 -7
  101. package/styles/search.css +56 -69
  102. package/styles/sidebar.css +211 -131
  103. package/styles/{variables.css → sl-variables.css} +3 -2
  104. package/styles/stldocs-variables.css +6 -0
  105. package/styles/toc.css +41 -34
  106. package/theme.css +10 -10
  107. package/tsconfig.json +2 -5
  108. package/virtual-module.d.ts +26 -4
  109. package/components/variables.css +0 -135
  110. package/stl-docs/components/mintlify-compat/Step.astro +0 -58
  111. package/stl-docs/components/mintlify-compat/Steps.astro +0 -17
  112. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  113. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  114. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  115. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  116. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  117. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  118. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  119. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  120. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
@@ -0,0 +1,450 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import { readFile } from 'fs/promises';
3
+ import { getProsePages } from '../shared/getProsePages';
4
+ import { getSharedLogger } from '../shared/getSharedLogger';
5
+ import { bold } from '../shared/terminalUtils';
6
+ import * as cheerio from 'cheerio';
7
+ import { toMarkdown } from './proseMarkdown/toMarkdown';
8
+ import { NormalizedStainlessDocsConfig } from './loadStlDocsConfig';
9
+ import { buildProseIndex } from '@stainless-api/docs-search/providers/algolia';
10
+
11
+ interface ContentBlock {
12
+ type: 'header' | 'content';
13
+ tag?: string;
14
+ id?: string;
15
+ text: string;
16
+ }
17
+
18
+ // Chunking configuration
19
+ // We target 64-256 tokens per chunk, using ~1.3 tokens/word for English text
20
+ const TOKENS_PER_WORD = 1.3;
21
+ const MIN_TOKENS = 64;
22
+ const MAX_TOKENS = 256;
23
+ const MIN_WORDS = Math.floor(MIN_TOKENS / TOKENS_PER_WORD); // ~49 words
24
+ const MAX_WORDS = Math.floor(MAX_TOKENS / TOKENS_PER_WORD); // ~197 words
25
+ const LINE_BREAK_WORDS = Math.floor((MAX_TOKENS * 0.75) / TOKENS_PER_WORD); // ~148 words
26
+ const SENTENCE_BREAK_WORDS = Math.floor((MAX_TOKENS * 0.875) / TOKENS_PER_WORD); // ~172 words
27
+
28
+ // Generate a URL-safe ID from header text (e.g., "OpenAPI Config" -> "openapi-config")
29
+ function slugify(text: string): string {
30
+ return text
31
+ .toLowerCase()
32
+ .replace(/`/g, '') // Remove backticks
33
+ .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
34
+ .replace(/^-|-$/g, ''); // Trim leading/trailing hyphens
35
+ }
36
+
37
+ // Check if a word ends with a real table cell boundary (| but not escaped \|)
38
+ function isTableCellBoundary(word: string): boolean {
39
+ return word.endsWith('|') && !word.endsWith('\\|');
40
+ }
41
+
42
+ /**
43
+ * Chunks content blocks into segments of 64-256 tokens.
44
+ *
45
+ * Chunking strategy:
46
+ * 1. Break at headers if chunk has >= MIN_WORDS, otherwise merge with next section
47
+ * 2. Prefer breaking at line/table boundaries after LINE_BREAK_WORDS (~148 words / ~192 tokens)
48
+ * 3. Break at sentence endings after SENTENCE_BREAK_WORDS (~172 words / ~224 tokens)
49
+ * 4. Force break at MAX_WORDS, preferring table row boundaries if available
50
+ * 5. Header context (id/tag) is preserved for continuation chunks
51
+ */
52
+ function chunkByWords(blocks: ContentBlock[]): { content: string; headerId?: string; headerTag?: string }[] {
53
+ const chunks: { content: string; headerId?: string; headerTag?: string }[] = [];
54
+
55
+ let currentChunk: string[] = [];
56
+ let currentHeaderId: string | undefined;
57
+ let currentHeaderTag: string | undefined;
58
+
59
+ // Flush current chunk to output. If splitAt is provided, keep words after that index for next chunk.
60
+ const flushChunk = (splitAt?: number) => {
61
+ if (currentChunk.length === 0) return;
62
+
63
+ const wordsToFlush = splitAt !== undefined ? currentChunk.slice(0, splitAt) : currentChunk;
64
+ const wordsToKeep = splitAt !== undefined ? currentChunk.slice(splitAt) : [];
65
+
66
+ if (wordsToFlush.length > 0) {
67
+ chunks.push({
68
+ content: wordsToFlush.join(' ').trim(),
69
+ headerId: currentHeaderId,
70
+ headerTag: currentHeaderTag,
71
+ });
72
+ }
73
+ currentChunk = wordsToKeep;
74
+ };
75
+
76
+ // Find a table row boundary to break at (between MIN_WORDS and current length)
77
+ // Returns the index to split at, or undefined if no good boundary found
78
+ const findTableRowBoundary = (): number | undefined => {
79
+ for (let i = currentChunk.length - 1; i >= MIN_WORDS; i--) {
80
+ const word = currentChunk[i]!;
81
+ const nextWord = currentChunk[i + 1];
82
+ // A row boundary is where one cell ends (|) and the next row starts (|)
83
+ if (isTableCellBoundary(word) && nextWord?.startsWith('|')) {
84
+ return i + 1;
85
+ }
86
+ }
87
+ return undefined;
88
+ };
89
+
90
+ for (const block of blocks) {
91
+ if (block.type === 'header') {
92
+ // Flush at header boundaries only if chunk meets minimum size
93
+ // This avoids creating tiny chunks for headers with little content
94
+ if (currentChunk.length >= MIN_WORDS) {
95
+ flushChunk();
96
+ }
97
+ currentHeaderId = block.id;
98
+ currentHeaderTag = block.tag;
99
+ // Include header text at the start of the new chunk
100
+ currentChunk.push(...block.text.split(/\s+/).filter((w) => w.length > 0));
101
+ continue;
102
+ }
103
+
104
+ // Split by newlines first to preserve line boundary information
105
+ const lines = block.text.split(/\n/);
106
+ let inCodeBlock = false;
107
+
108
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
109
+ const line = lines[lineIdx]!;
110
+
111
+ // Track code block boundaries
112
+ if (/^(`{3,}|~{3,})/.test(line.trim())) {
113
+ inCodeBlock = !inCodeBlock;
114
+ }
115
+
116
+ // Calculate indentation level (number of leading spaces, treating tabs as 2 spaces)
117
+ const indentMatch = line.match(/^(\s*)/);
118
+ const indentLevel = indentMatch ? indentMatch[1]!.replace(/\t/g, ' ').length : 0;
119
+
120
+ const words = line.split(/\s+/).filter((w) => w.length > 0);
121
+ const isLastLine = lineIdx === lines.length - 1;
122
+
123
+ for (let wordIdx = 0; wordIdx < words.length; wordIdx++) {
124
+ const word = words[wordIdx]!;
125
+ const isEndOfLine = wordIdx === words.length - 1 && !isLastLine;
126
+
127
+ if (currentChunk.length >= MAX_WORDS) {
128
+ flushChunk(findTableRowBoundary());
129
+ }
130
+
131
+ currentChunk.push(word);
132
+
133
+ // In code blocks, avoid early flushes to keep blocks together
134
+ // - Light indentation (2+ spaces): require more words before flushing
135
+ // - Deep indentation (4+ spaces): skip early flushes entirely
136
+ const inShallowCode = inCodeBlock && indentLevel >= 2 && indentLevel < 4;
137
+ const inDeepCode = inCodeBlock && indentLevel >= 4;
138
+
139
+ // Flush early at natural break points
140
+ const len = currentChunk.length;
141
+ const atTableBreak = len >= LINE_BREAK_WORDS && isTableCellBoundary(word);
142
+ // Shallow code: only flush at sentence threshold; Deep code: don't flush early
143
+ const lineBreakThreshold = inShallowCode ? SENTENCE_BREAK_WORDS : LINE_BREAK_WORDS;
144
+ const atLineBreak = len >= lineBreakThreshold && isEndOfLine && !inDeepCode;
145
+ const atSentenceBreak = len >= SENTENCE_BREAK_WORDS && /[.!?]["']?$/.test(word) && !inDeepCode;
146
+ if (atTableBreak || atLineBreak || atSentenceBreak) {
147
+ flushChunk();
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ flushChunk();
154
+ return chunks;
155
+ }
156
+
157
+ /**
158
+ * Parses markdown into content blocks, identifying headers and content sections.
159
+ * Tracks fenced code blocks to avoid treating # comments in code as headers.
160
+ */
161
+ function parseMarkdown(markdown: string): ContentBlock[] {
162
+ const blocks: ContentBlock[] = [];
163
+
164
+ // Extract title from frontmatter and treat it as h1
165
+ const frontmatterMatch = markdown.match(/^---\n([\s\S]*?)\n---/);
166
+ if (frontmatterMatch) {
167
+ const frontmatter = frontmatterMatch[1]!;
168
+ const titleMatch = frontmatter.match(/^title:\s*(.+)$/m);
169
+ if (titleMatch) {
170
+ const title = titleMatch[1]!.trim().replace(/^["']|["']$/g, ''); // Remove quotes if present
171
+ blocks.push({
172
+ type: 'header',
173
+ tag: 'h1',
174
+ id: slugify(title),
175
+ text: title,
176
+ });
177
+ }
178
+ }
179
+
180
+ // Remove frontmatter
181
+ const content = markdown.replace(/^---[\s\S]*?---\n*/, '').trim();
182
+
183
+ // Split into lines and process
184
+ const lines = content.split('\n');
185
+ let currentContent: string[] = [];
186
+ let inCodeBlock = false;
187
+
188
+ const flushContent = () => {
189
+ const text = currentContent.join('\n').trim();
190
+ if (text) {
191
+ blocks.push({ type: 'content', text });
192
+ }
193
+ currentContent = [];
194
+ };
195
+
196
+ for (const line of lines) {
197
+ // Track fenced code blocks (``` or ~~~)
198
+ // Only match standalone markers: ```[language] with nothing else on the line
199
+ // This avoids matching inline code blocks in table cells like "``` Then content..."
200
+ if (/^(`{3,}|~{3,})([a-zA-Z0-9]*)?(\s*)$/.test(line)) {
201
+ inCodeBlock = !inCodeBlock;
202
+ currentContent.push(line);
203
+ continue;
204
+ }
205
+
206
+ // Only match headers outside of code blocks
207
+ if (!inCodeBlock) {
208
+ const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
209
+
210
+ if (headerMatch) {
211
+ flushContent();
212
+ const level = headerMatch[1]!.length;
213
+ const headerText = headerMatch[2]!.trim();
214
+ blocks.push({
215
+ type: 'header',
216
+ tag: `h${level}`,
217
+ id: slugify(headerText),
218
+ text: headerText,
219
+ });
220
+ continue;
221
+ }
222
+ }
223
+
224
+ currentContent.push(line);
225
+ }
226
+
227
+ flushContent();
228
+ return blocks;
229
+ }
230
+
231
+ /**
232
+ * Extracts and chunks markdown content for search indexing.
233
+ * Yields chunk objects with content, header context, and chunk metadata.
234
+ */
235
+ export function* indexMarkdown(markdown: string) {
236
+ const blocks = parseMarkdown(markdown);
237
+ const chunks = chunkByWords(blocks);
238
+ const documentId = crypto.randomUUID();
239
+
240
+ for (const [index, chunk] of chunks.entries()) {
241
+ yield {
242
+ id: chunk.headerId ?? '',
243
+ tag: chunk.headerTag ?? '',
244
+ content: chunk.content,
245
+ chunk: {
246
+ id: documentId,
247
+ index,
248
+ total: chunks.length,
249
+ },
250
+ };
251
+ }
252
+ }
253
+
254
+ function chunkHTMLByWords(content: string, chunkSize: number = 30000, chunkOverlap: number = 10) {
255
+ if (Buffer.byteLength(content) < chunkSize) return [content];
256
+
257
+ const words = content.split(/\s+/);
258
+ const chunks: string[] = [];
259
+
260
+ let currentChunk: string[] = [];
261
+ let currentSize = 0;
262
+
263
+ for (const word of words) {
264
+ const wordSize = Buffer.byteLength(word + ' ', 'utf-8');
265
+
266
+ if (currentSize + wordSize > chunkSize && currentChunk.length > 0) {
267
+ chunks.push(currentChunk.join(' '));
268
+
269
+ const overlapStart = Math.max(0, currentChunk.length - chunkOverlap);
270
+ currentChunk = currentChunk.slice(overlapStart);
271
+ currentSize = Buffer.byteLength(currentChunk.join(' '), 'utf-8');
272
+ }
273
+
274
+ currentChunk.push(word);
275
+ currentSize += wordSize;
276
+ }
277
+
278
+ if (currentChunk.length > 0) {
279
+ chunks.push(currentChunk.join(' '));
280
+ }
281
+
282
+ return chunks;
283
+ }
284
+
285
+ export function* indexHTML(content: string, root: string, pattern: string) {
286
+ const $ = cheerio.load(content);
287
+ const matches = $(root).find(pattern);
288
+
289
+ for (const match of matches) {
290
+ const rawText = $(match).text().trim();
291
+ const chunks = chunkHTMLByWords(rawText);
292
+ const chunkId = crypto.randomUUID();
293
+
294
+ for (const [chunkN, content] of chunks.entries()) {
295
+ yield {
296
+ id: $(match).attr('id'),
297
+ tag: match.tagName.toLowerCase(),
298
+ content,
299
+ chunk: {
300
+ id: chunkId,
301
+ index: chunkN,
302
+ total: chunks.length,
303
+ },
304
+ };
305
+ }
306
+ }
307
+ }
308
+
309
+ const root = 'main';
310
+ const pattern = 'h1, h2, h3, h4, h5, h6, p, li';
311
+
312
+ export function stainlessDocsAlgoliaProseIndexing({
313
+ apiReferenceBasePath,
314
+ }: {
315
+ apiReferenceBasePath: string | null;
316
+ }): AstroIntegration {
317
+ return {
318
+ name: 'stl-docs-prose-indexing',
319
+ hooks: {
320
+ 'astro:build:done': async ({ logger: localLogger, dir }) => {
321
+ const logger = getSharedLogger({ fallback: localLogger });
322
+ const outputBasePath = dir.pathname;
323
+
324
+ const {
325
+ PUBLIC_ALGOLIA_APP_ID: appId,
326
+ PUBLIC_ALGOLIA_INDEX: indexName,
327
+ PRIVATE_ALGOLIA_WRITE_KEY: algoliaWriteKey,
328
+ } = process.env;
329
+
330
+ if (!appId || !indexName || !algoliaWriteKey) {
331
+ logger.info('Skipping algolia indexing due to missing environment variables');
332
+ return;
333
+ }
334
+
335
+ const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
336
+ logger.info(bold(`Indexing ${pagesToRender.length} prose pages for algolia search`));
337
+
338
+ const objects = [];
339
+ for (const absHtmlPath of pagesToRender) {
340
+ const content = await readFile(absHtmlPath, 'utf-8');
341
+ const idx = indexHTML(content, root, pattern);
342
+ for (const entry of idx)
343
+ objects.push({
344
+ ...entry,
345
+ source: absHtmlPath.slice(outputBasePath.length),
346
+ });
347
+ }
348
+
349
+ try {
350
+ await buildProseIndex(appId, `${indexName}-prose`, algoliaWriteKey, objects);
351
+ } catch (err) {
352
+ logger.error(`Failed to index prose content: ${err}`);
353
+ }
354
+ },
355
+ },
356
+ };
357
+ }
358
+
359
+ export function stainlessDocsVectorProseIndexing(
360
+ config: NormalizedStainlessDocsConfig,
361
+ apiReferenceBasePath: string | null,
362
+ ): AstroIntegration {
363
+ return {
364
+ name: 'stl-docs-prose-indexing',
365
+ hooks: {
366
+ 'astro:build:done': async ({ logger: localLogger, dir }) => {
367
+ const logger = getSharedLogger({ fallback: localLogger });
368
+ const outputBasePath = dir.pathname;
369
+
370
+ const stainlessProjectName = config.apiReference?.stainlessProject;
371
+
372
+ const {
373
+ STAINLESS_API_KEY: stainlessApiKey,
374
+ STAINLESS_DOCS_SITE_ID: stainlessDocsSiteId,
375
+ STAINLESS_DOCS_REPO_SHA: stainlessDocsRepoSha,
376
+ } = process.env;
377
+
378
+ // Skip indexing if required environment variables are not set
379
+ if (!stainlessApiKey || !stainlessProjectName || !stainlessDocsSiteId || !stainlessDocsRepoSha) {
380
+ logger.info(
381
+ `Skipping vector prose search indexing: required environment/config variables not set, missing: ${[
382
+ !stainlessApiKey && 'STAINLESS_API_KEY',
383
+ !stainlessDocsSiteId && 'STAINLESS_DOCS_SITE_ID',
384
+ !stainlessDocsRepoSha && 'STAINLESS_DOCS_REPO_SHA',
385
+ !stainlessProjectName && 'stainlessProject in apiReference config',
386
+ ]
387
+ .filter(Boolean)
388
+ .join(', ')}`,
389
+ );
390
+ return;
391
+ }
392
+
393
+ const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
394
+
395
+ if (pagesToRender.length === 0) {
396
+ logger.info('No prose pages found to index for vector search');
397
+ return;
398
+ }
399
+
400
+ logger.info(bold(`Indexing ${pagesToRender.length} prose pages for vector search`));
401
+
402
+ const objects: {
403
+ id: string;
404
+ tag: string;
405
+ content: string;
406
+ kind: 'prose';
407
+ source: string;
408
+ }[] = [];
409
+ for (const absHtmlPath of pagesToRender) {
410
+ const content = await readFile(absHtmlPath, 'utf-8');
411
+ const markdown = await toMarkdown(content);
412
+
413
+ if (markdown) {
414
+ const idx = indexMarkdown(markdown);
415
+ for (const { chunk, ...entry } of idx)
416
+ objects.push({
417
+ ...entry,
418
+ kind: 'prose',
419
+ source: absHtmlPath.slice(outputBasePath.length),
420
+ });
421
+ }
422
+ }
423
+
424
+ if (objects.length === 0) {
425
+ logger.info('No prose content extracted to index for vector search');
426
+ return;
427
+ }
428
+
429
+ logger.info(bold(`Uploading ${objects.length} prose content chunks to stainless docs index`));
430
+
431
+ const response = await fetch(
432
+ `https://api.stainless.com/api/projects/${stainlessProjectName}/docs-sites/${stainlessDocsSiteId}/index`,
433
+ {
434
+ method: 'POST',
435
+ headers: {
436
+ 'Content-Type': 'application/json',
437
+ Authorization: `Bearer ${stainlessApiKey}`,
438
+ },
439
+ body: JSON.stringify({
440
+ docs_repo_sha: stainlessDocsRepoSha,
441
+ index: objects,
442
+ }),
443
+ },
444
+ );
445
+
446
+ console.log(`docs index API response code ${response.status}: ${await response.text()}`);
447
+ },
448
+ },
449
+ };
450
+ }
@@ -58,7 +58,7 @@ function getTabIndexForSlug(
58
58
  match: 'exact' | 'prefix';
59
59
  } | null {
60
60
  // ↓ exact match eg. slug = "/blog" and there is a link containing "/blog"
61
- let tab = linksByTab.get(slug);
61
+ const tab = linksByTab.get(slug)!;
62
62
  if (typeof tab === 'string') {
63
63
  return {
64
64
  match: 'exact',
@@ -88,13 +88,19 @@ function getNonSplitLinksByTab() {
88
88
  const linksByTab = new Map<string, string>();
89
89
 
90
90
  for (let i = 0; i < TABS.length; i++) {
91
- const tab = TABS[i];
91
+ const tab = TABS[i]!;
92
92
  linksByTab.set(tab.link, String(i));
93
93
  }
94
94
 
95
95
  return linksByTab;
96
96
  }
97
97
 
98
+ export interface StarlightRouteWithStlDocs extends StarlightRouteData {
99
+ _stlDocs?: {
100
+ activeTabIndex: number;
101
+ };
102
+ }
103
+
98
104
  export const onRequest = defineRouteMiddleware(async (context) => {
99
105
  // if using content collection schema, use: context.locals.starlightRoute.entry.data.stainlessStarlight
100
106
  // this worked without collections but relied on hijacking starlightRoute: context.props.frontmatter.stainlessStarlight
@@ -143,7 +149,8 @@ export const onRequest = defineRouteMiddleware(async (context) => {
143
149
  }
144
150
 
145
151
  // We store the active tab index so we can use it in our nav tabs component
146
- context.locals.starlightRoute._stlStarlight = {
152
+ const routeData: StarlightRouteWithStlDocs = context.locals.starlightRoute;
153
+ routeData._stlDocs = {
147
154
  activeTabIndex: activeTabIndex.index,
148
155
  };
149
156
 
@@ -179,5 +186,6 @@ export const onRequest = defineRouteMiddleware(async (context) => {
179
186
 
180
187
  matchingGroup?.entries.unshift(...mobileLinks);
181
188
 
189
+ (context.locals._stlStarlightPage ??= {}).fullSidebar = context.locals.starlightRoute.sidebar;
182
190
  context.locals.starlightRoute.sidebar = matchingGroup.entries;
183
191
  });