@stainless-api/docs 0.1.0-beta.13 → 0.1.0-beta.130

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 (188) hide show
  1. package/CHANGELOG.md +1102 -0
  2. package/ambient.d.ts +6 -0
  3. package/eslint-suppressions.json +90 -0
  4. package/{eslint.config.js → eslint.config.ts} +0 -2
  5. package/locals.d.ts +17 -0
  6. package/package.json +62 -44
  7. package/playground-virtual-modules.d.ts +96 -0
  8. package/plugin/assets/languages/cli.svg +14 -0
  9. package/plugin/assets/languages/csharp.svg +1 -0
  10. package/plugin/assets/languages/php.svg +4 -0
  11. package/plugin/buildAlgoliaIndex.ts +40 -39
  12. package/plugin/components/MethodDescription.tsx +54 -0
  13. package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
  14. package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
  15. package/plugin/components/RequestBuilder/index.tsx +40 -0
  16. package/plugin/components/RequestBuilder/props.ts +9 -0
  17. package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
  18. package/plugin/components/RequestBuilder/styles.css +67 -0
  19. package/plugin/components/SDKSelect.astro +18 -111
  20. package/plugin/components/SnippetCode.tsx +112 -70
  21. package/plugin/components/StainlessIslands.tsx +126 -0
  22. package/plugin/components/search/SearchAlgolia.astro +46 -29
  23. package/plugin/components/search/SearchIsland.tsx +61 -37
  24. package/plugin/generateAPIReferenceLink.ts +0 -40
  25. package/plugin/globalJs/ai-dropdown-options.ts +248 -0
  26. package/plugin/globalJs/code-snippets.ts +45 -16
  27. package/plugin/globalJs/copy.ts +115 -27
  28. package/plugin/globalJs/create-playground.shim.ts +3 -0
  29. package/plugin/globalJs/method-descriptions.ts +33 -0
  30. package/plugin/globalJs/navigation.ts +24 -44
  31. package/plugin/globalJs/playground-data.shim.ts +1 -0
  32. package/plugin/globalJs/playground-data.ts +14 -0
  33. package/plugin/globalJs/summary-selection-tweak.ts +29 -0
  34. package/plugin/helpers/generateDocsRoutes.ts +59 -0
  35. package/plugin/helpers/multiSpec.ts +8 -0
  36. package/plugin/index.ts +317 -141
  37. package/plugin/languages.ts +8 -2
  38. package/plugin/loadPluginConfig.ts +284 -109
  39. package/plugin/markdown/highlighter.ts +100 -0
  40. package/plugin/markdown/index.ts +39 -0
  41. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
  42. package/plugin/react/Routing.tsx +98 -263
  43. package/plugin/referencePlaceholderUtils.ts +17 -14
  44. package/plugin/replaceSidebarPlaceholderMiddleware.ts +39 -35
  45. package/plugin/routes/Docs.astro +72 -111
  46. package/plugin/routes/DocsStatic.astro +6 -5
  47. package/plugin/routes/Overview.astro +46 -22
  48. package/plugin/routes/llms.ts +186 -0
  49. package/plugin/routes/markdown.ts +13 -12
  50. package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +84 -69
  51. package/plugin/specs/FileCache.ts +99 -0
  52. package/plugin/specs/fetchSpecSSR.ts +27 -0
  53. package/plugin/specs/generateSpec.ts +112 -0
  54. package/plugin/specs/index.ts +132 -0
  55. package/plugin/specs/inputResolver.ts +148 -0
  56. package/plugin/{cms → specs}/worker.ts +82 -5
  57. package/plugin/vendor/preview.worker.docs.js +27121 -16890
  58. package/plugin/vendor/templates/cli.md +1 -0
  59. package/plugin/vendor/templates/go.md +4 -2
  60. package/plugin/vendor/templates/java.md +5 -1
  61. package/plugin/vendor/templates/kotlin.md +5 -1
  62. package/plugin/vendor/templates/node.md +4 -2
  63. package/plugin/vendor/templates/python.md +4 -2
  64. package/plugin/vendor/templates/ruby.md +4 -2
  65. package/plugin/vendor/templates/terraform.md +1 -1
  66. package/plugin/vendor/templates/typescript.md +3 -1
  67. package/resolveSrcFile.ts +10 -0
  68. package/scripts/vendor_deps.ts +5 -5
  69. package/shared/conditionalIntegration.ts +28 -0
  70. package/shared/getProsePages.ts +41 -0
  71. package/shared/getSharedLogger.ts +15 -0
  72. package/shared/terminalUtils.ts +3 -0
  73. package/shared/virtualModule.ts +46 -1
  74. package/src/content.config.ts +9 -0
  75. package/stl-docs/aiChatExamples.ts +95 -0
  76. package/stl-docs/chat/docs-chat-handler.ts +18 -0
  77. package/stl-docs/chat/hook.ts +215 -0
  78. package/stl-docs/chat/schemas.ts +70 -0
  79. package/stl-docs/chat/stainless-handler/index.ts +126 -0
  80. package/stl-docs/chat/stream-util.ts +16 -0
  81. package/stl-docs/chat/ui/AiChat.module.css +591 -0
  82. package/stl-docs/chat/ui/AiChat.tsx +188 -0
  83. package/stl-docs/chat/ui/Trigger.tsx +154 -0
  84. package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
  85. package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
  86. package/stl-docs/chat/ui/components/ChatLog.tsx +96 -0
  87. package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
  88. package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
  89. package/stl-docs/chat/ui/components/MessageFeedback.tsx +109 -0
  90. package/stl-docs/chat/ui/components/Table.tsx +15 -0
  91. package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
  92. package/stl-docs/chat/ui/components/hljs-github.css +81 -0
  93. package/stl-docs/chat/ui/scroll-manager.ts +86 -0
  94. package/stl-docs/chat/ui/types.ts +45 -0
  95. package/stl-docs/components/AIDropdown.tsx +63 -0
  96. package/stl-docs/components/AiChatIsland.tsx +16 -0
  97. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
  98. package/stl-docs/components/ContentPanel.astro +9 -0
  99. package/stl-docs/components/Footer.astro +89 -0
  100. package/stl-docs/components/Head.astro +20 -0
  101. package/stl-docs/components/Header.astro +3 -9
  102. package/stl-docs/components/PageFrame.astro +37 -0
  103. package/stl-docs/components/PageSidebar.astro +11 -0
  104. package/stl-docs/components/PageTitle.astro +82 -0
  105. package/stl-docs/components/StainlessLogo.svg +4 -0
  106. package/stl-docs/components/ThemeProvider.astro +36 -0
  107. package/stl-docs/components/ThemeSelect.astro +84 -146
  108. package/stl-docs/components/TwoColumnContent.astro +2 -0
  109. package/stl-docs/components/headers/DefaultHeader.astro +6 -8
  110. package/stl-docs/components/headers/StackedHeader.astro +10 -53
  111. package/stl-docs/components/icons/chat-gpt.tsx +2 -2
  112. package/stl-docs/components/icons/cursor.tsx +10 -0
  113. package/stl-docs/components/icons/gemini.tsx +19 -0
  114. package/stl-docs/components/icons/markdown.tsx +1 -1
  115. package/stl-docs/components/index.ts +1 -0
  116. package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
  117. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
  118. package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
  119. package/stl-docs/components/mintlify-compat/Frame.astro +6 -6
  120. package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
  121. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
  122. package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
  123. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
  124. package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
  125. package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
  126. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
  127. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
  128. package/stl-docs/components/mintlify-compat/card.css +4 -4
  129. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  130. package/stl-docs/components/nav-tabs/NavDropdown.astro +38 -77
  131. package/stl-docs/components/nav-tabs/NavTabs.astro +81 -81
  132. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +1 -2
  133. package/stl-docs/components/nav-tabs/buildNavLinks.ts +5 -2
  134. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  135. package/stl-docs/components/pagination/Pagination.astro +177 -0
  136. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  137. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  138. package/stl-docs/components/pagination/util.ts +71 -0
  139. package/stl-docs/components/scripts.ts +1 -0
  140. package/stl-docs/components/sidebars/BaseSidebar.astro +80 -2
  141. package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
  142. package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
  143. package/stl-docs/disableCalloutSyntax.ts +36 -0
  144. package/stl-docs/fonts.ts +186 -0
  145. package/stl-docs/index.ts +176 -58
  146. package/stl-docs/loadStlDocsConfig.ts +73 -8
  147. package/stl-docs/proseDocSync.test.ts +74 -0
  148. package/stl-docs/proseDocSync.ts +344 -0
  149. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
  150. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
  151. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  152. package/stl-docs/proseSearchIndexing.ts +218 -0
  153. package/stl-docs/tabsMiddleware.ts +14 -5
  154. package/styles/code.css +53 -49
  155. package/styles/links.css +2 -37
  156. package/styles/method-descriptions.css +36 -0
  157. package/styles/overrides.css +28 -46
  158. package/styles/page.css +228 -38
  159. package/styles/sdk_select.css +9 -6
  160. package/styles/search.css +11 -21
  161. package/styles/sidebar.css +28 -215
  162. package/styles/{variables.css → sl-variables.css} +4 -8
  163. package/styles/stldocs-variables.css +6 -0
  164. package/styles/toc.css +19 -8
  165. package/theme.css +11 -9
  166. package/tsconfig.json +1 -4
  167. package/virtual-module.d.ts +66 -8
  168. package/components/variables.css +0 -112
  169. package/plugin/cms/client.ts +0 -62
  170. package/plugin/cms/server.ts +0 -268
  171. package/plugin/globalJs/ai-dropdown.ts +0 -57
  172. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
  173. package/stl-docs/components/ClientRouterHead.astro +0 -41
  174. package/stl-docs/components/content-panel/ContentPanel.astro +0 -69
  175. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
  176. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
  177. package/stl-docs/components/mintlify-compat/Step.astro +0 -56
  178. package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
  179. package/styles/fonts.css +0 -68
  180. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  181. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  182. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  183. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  184. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  185. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  186. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  187. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  188. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
package/stl-docs/index.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import starlight from '@astrojs/starlight';
2
2
  import react from '@astrojs/react';
3
- import { stainlessStarlight } from '../plugin';
3
+ import type { StarlightPlugin } from '@astrojs/starlight/types';
4
+ import { disableCalloutSyntaxStarlightPlugin } from './disableCalloutSyntax';
4
5
 
5
6
  import type { AstroIntegration } from 'astro';
6
7
 
7
8
  import { normalizeRedirects, type NormalizedRedirectConfig } from './redirects';
8
- import { join } from 'path';
9
+ import path from 'node:path';
9
10
  import { mkdirSync, writeFileSync } from 'fs';
10
11
  import {
11
12
  parseStlDocsConfig,
@@ -16,11 +17,21 @@ import {
16
17
  type StarlightSidebarConfig,
17
18
  } from './loadStlDocsConfig';
18
19
  import { buildVirtualModuleString } from '../shared/virtualModule';
19
-
20
- import geistPath from '../plugin/assets/fonts/geist/geist-latin.woff2?url';
20
+ import type * as StlDocsVirtualModule from 'virtual:stl-docs-virtual-module';
21
+ import { resolveSrcFile } from '../resolveSrcFile';
22
+ import { stainlessDocsMarkdownRenderer } from './proseMarkdown/proseMarkdownIntegration';
23
+ import { getSharedLogger, setSharedLogger } from '../shared/getSharedLogger';
24
+ import { stainlessDocsVectorProseIndexing } from './proseDocSync';
25
+ import { stainlessDocsAlgoliaProseIndexing } from './proseSearchIndexing';
26
+ import { stainlessStarlight } from '../plugin';
27
+ import { getFontRoles, flattenFonts } from './fonts';
28
+ import conditionalIntegration from '../shared/conditionalIntegration';
29
+ import generateExamplesPlugin from './aiChatExamples';
21
30
 
22
31
  export * from '../plugin';
23
32
 
33
+ const COMPONENTS_FOLDER = '/stl-docs/components';
34
+
24
35
  function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig) {
25
36
  // We transform our tabs into a Starlight sidebar
26
37
  // This gives them all the built-in features of Starlight (eg. auto-generated entries by directory)
@@ -40,22 +51,48 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
40
51
  }
41
52
 
42
53
  type ComponentOverrides = StarlightConfigDefined['components'];
43
- const plugins = [...config.starlightCompat.plugins];
44
-
45
54
  const componentOverrides: ComponentOverrides = {
46
- Sidebar: '@stainless-api/docs/BaseSidebar',
47
- Header: '@stainless-api/docs/Header',
48
- ThemeSelect: '@stainless-api/docs/ThemeSelect',
49
- ContentPanel: '@stainless-api/docs/ContentPanel',
50
- TableOfContents: '@stainless-api/docs/TableOfContents',
55
+ Head: resolveSrcFile(COMPONENTS_FOLDER, './Head.astro'),
56
+
57
+ PageFrame: resolveSrcFile(COMPONENTS_FOLDER, './PageFrame.astro'),
58
+ TwoColumnContent: resolveSrcFile(COMPONENTS_FOLDER, './TwoColumnContent.astro'),
59
+
60
+ Header: resolveSrcFile(COMPONENTS_FOLDER, './Header.astro'),
61
+ ThemeProvider: resolveSrcFile(COMPONENTS_FOLDER, './ThemeProvider.astro'),
62
+ ThemeSelect: resolveSrcFile(COMPONENTS_FOLDER, './ThemeSelect.astro'),
63
+
64
+ Sidebar: resolveSrcFile(COMPONENTS_FOLDER, './sidebars/BaseSidebar.astro'),
65
+ ContentPanel: resolveSrcFile(COMPONENTS_FOLDER, './ContentPanel.astro'),
66
+ PageTitle: resolveSrcFile(COMPONENTS_FOLDER, './PageTitle.astro'),
67
+ PageSidebar: resolveSrcFile(COMPONENTS_FOLDER, './PageSidebar.astro'),
68
+ TableOfContents: resolveSrcFile(COMPONENTS_FOLDER, './TableOfContents.astro'),
69
+
70
+ Footer: resolveSrcFile(COMPONENTS_FOLDER, './Footer.astro'),
71
+ Pagination: resolveSrcFile(COMPONENTS_FOLDER, './pagination/Pagination.astro'),
51
72
  };
52
73
 
53
- if (config.apiReference !== null) {
54
- componentOverrides.Sidebar = '@stainless-api/docs/SDKSelectSidebar';
55
- componentOverrides.Search = '@stainless-api/docs/Search';
56
- plugins.unshift(stainlessStarlight(config.apiReference));
74
+ const plugins: StarlightPlugin[] = [
75
+ // Disable starlight callout syntax in favor of our own component
76
+ disableCalloutSyntaxStarlightPlugin,
77
+ ];
78
+
79
+ if (config.apiReference) {
80
+ plugins.push(
81
+ stainlessStarlight({
82
+ ...config.apiReference,
83
+ contextMenu: config.contextMenu,
84
+ experimentalPrerender:
85
+ config.apiReference.experimentalPrerender === undefined
86
+ ? config.starlightPassThrough.prerender
87
+ : config.apiReference.experimentalPrerender,
88
+ }),
89
+ );
90
+ componentOverrides.Sidebar = resolveSrcFile(COMPONENTS_FOLDER, './sidebars/SDKSelectSidebar.astro');
91
+ componentOverrides.Search = resolveSrcFile('/plugin/components/search/Search.astro');
57
92
  }
58
93
 
94
+ plugins.push(...config.starlightCompat.plugins, ...config.plugins.map((p) => p(config)));
95
+
59
96
  // TODO: re-add once we figure out what to do with the client router
60
97
  // if (config.enableClientRouter) {
61
98
  // // logger.info(`Client router is enabled`);
@@ -64,8 +101,11 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
64
101
  // // logger.info(`Client router is disabled`);
65
102
  // }
66
103
 
104
+ const userExpressiveCode = typeof config.expressiveCode === 'object' ? config.expressiveCode : {};
105
+
67
106
  return starlight({
68
107
  ...config.starlightPassThrough,
108
+ pagefind: config.starlightCompat.pagefind,
69
109
  sidebar,
70
110
  components: {
71
111
  ...componentOverrides,
@@ -87,68 +127,110 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
87
127
  setupNavLinksInitial();
88
128
  `,
89
129
  },
90
- // TODO: for users who are overriding the font stack in their own styles, how can we know that
91
- // and preload their font instead of ours?
92
- {
93
- tag: 'link',
94
- attrs: {
95
- rel: 'preload',
96
- as: 'font',
97
- type: 'font/woff2',
98
- crossorigin: 'anonymous',
99
- href: geistPath,
130
+ ],
131
+ routeMiddleware: [
132
+ ...config.starlightCompat.routeMiddleware,
133
+ resolveSrcFile('/stl-docs/tabsMiddleware.ts'),
134
+ ],
135
+ customCss: [resolveSrcFile('/theme.css'), ...config.customCss],
136
+
137
+ expressiveCode: {
138
+ ...userExpressiveCode,
139
+ themes: userExpressiveCode.themes ?? ['github-light', 'github-dark'],
140
+ styleOverrides: {
141
+ ...userExpressiveCode.styleOverrides,
142
+ textMarkers: {
143
+ insBackground: 'var(--stl-color-green-muted-background)',
144
+ insBorderColor: 'var(--stl-color-green-border)',
145
+ insDiffIndicatorColor: 'var(--stl-color-green-foreground-reduced)',
146
+
147
+ delBackground: 'var(--stl-color-red-muted-background)',
148
+ delBorderColor: 'var(--stl-color-red-border)',
149
+ delDiffIndicatorColor: 'var(--stl-color-red-foreground-reduced)',
150
+
151
+ markBackground: 'var(--stl-color-blue-muted-background)',
152
+ markBorderColor: 'var(--stl-color-blue-border)',
153
+ ...userExpressiveCode.styleOverrides?.textMarkers,
100
154
  },
101
155
  },
102
- ],
103
- routeMiddleware: [...config.starlightCompat.routeMiddleware, '@stainless-api/docs/tabsMiddleware'],
104
- customCss: ['@stainless-api/docs/theme', ...config.customCss],
156
+ },
105
157
  plugins,
106
158
  });
107
159
  }
108
160
 
109
- function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroIntegration {
110
- const virtualId = `virtual:stl-docs-virtual-module`;
111
- // The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
112
- const resolvedId = `\0${virtualId}`;
161
+ function stainlessDocsIntegration(
162
+ config: NormalizedStainlessDocsConfig,
163
+ apiReferenceBasePath: string | null,
164
+ ): AstroIntegration {
113
165
  let redirects: NormalizedRedirectConfig | null = null;
114
166
 
115
167
  return {
116
- name: 'stl-docs-integration',
168
+ name: 'stl-docs-astro',
117
169
  hooks: {
118
- 'astro:config:setup': ({ updateConfig, command, config: astroConfig }) => {
119
- // // we only handle redirects for builds
120
- // // in dev, Astro handles them for us
170
+ 'astro:config:setup': async ({ updateConfig, command, config: astroConfig, logger: localLogger }) => {
171
+ const logger = getSharedLogger({ fallback: localLogger });
172
+ // we only handle redirects for builds
173
+ // in dev, Astro handles them for us
121
174
  if (command === 'build' && astroConfig.redirects) {
122
175
  redirects = normalizeRedirects(astroConfig.redirects);
123
176
  }
124
177
 
178
+ const base = astroConfig.base ?? '/';
179
+ const withBase = (link: string) =>
180
+ /^([a-z][a-z0-9+.-]*:|\/\/)/.test(link) ? link : path.posix.join(base, link);
181
+
182
+ const virtualModules = new Map(
183
+ Object.entries({
184
+ 'virtual:stl-docs-virtual-module': buildVirtualModuleString({
185
+ TABS: config.tabs.map((tab) => ({ ...tab, link: withBase(tab.link) })),
186
+ SPLIT_TABS_ENABLED: config.splitTabsEnabled,
187
+ HEADER_LINKS: config.header.links.map((link) => ({ ...link, link: withBase(link.link) })),
188
+ HEADER_LAYOUT: config.header.layout,
189
+ ENABLE_CLIENT_ROUTER: config.enableClientRouter,
190
+ API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
191
+ ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
192
+ ENABLE_CONTEXT_MENU: config.contextMenu, // TODO: do not duplicate this between both virtual modules
193
+ RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
194
+ FONTS: getFontRoles(config.fonts),
195
+ LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: config.linkGroupTitlesToOverviewPages,
196
+ RENDER_CREDITS: config.credits,
197
+ SITE_TITLE: config.siteTitle,
198
+ ENABLE_AI_CHAT: !!config.aiChat,
199
+ } satisfies typeof StlDocsVirtualModule),
200
+ }),
201
+ );
202
+
125
203
  updateConfig({
204
+ fonts: [...flattenFonts(config.fonts), ...(astroConfig?.fonts ?? [])],
126
205
  vite: {
127
- ssr: {
128
- noExternal: ['@stainless-api/ui-primitives'],
206
+ define: {
207
+ __STLDOCS_HAS_API_REFERENCE__: !!config.apiReference,
208
+ __STLDOCS_ENABLE_AI_CHAT__: !!config.aiChat,
129
209
  },
130
- optimizeDeps: { include: ['@stainless-api/ui-primitives'] },
131
210
  plugins: [
132
211
  {
133
- name: 'stl-docs-vite',
212
+ name: 'stl-docs-virtual-modules',
134
213
  resolveId(id) {
135
- if (id === virtualId) {
136
- return resolvedId;
137
- }
214
+ // The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
215
+ if (virtualModules.has(id)) return `\0${id}`;
138
216
  },
139
217
  load(id) {
140
- if (id === resolvedId) {
141
- return buildVirtualModuleString({
142
- TABS: config.tabs,
143
- SPLIT_TABS_ENABLED: config.splitTabsEnabled,
144
- HEADER_LINKS: config.header.links,
145
- HEADER_LAYOUT: config.header.layout,
146
- ENABLE_CLIENT_ROUTER: config.enableClientRouter,
147
- INCLUDE_AI_DROPDOWN_OPTIONS: config.includeAIDropdownOptions,
148
- });
149
- }
218
+ const bare = id.replace(/^\0/, '');
219
+ if (virtualModules.has(bare)) return virtualModules.get(bare);
150
220
  },
151
221
  },
222
+ // Separate plugin for the examples because it has async resolution; not a simple string
223
+ // like the above plugins
224
+ ...(config.aiChat
225
+ ? [
226
+ await generateExamplesPlugin({
227
+ projectName: config.apiReference?.stainlessProject ?? undefined,
228
+ logger,
229
+ exampleOverrides:
230
+ typeof config.aiChat === 'object' ? config.aiChat.exampleOverrides : undefined,
231
+ }),
232
+ ]
233
+ : []),
152
234
  ],
153
235
  },
154
236
  build: {
@@ -159,9 +241,9 @@ function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroI
159
241
  },
160
242
  'astro:build:done': ({ dir }) => {
161
243
  if (redirects !== null) {
162
- const stainlessDir = join(dir.pathname, '_stainless');
163
- mkdirSync(stainlessDir);
164
- const outputPath = join(stainlessDir, 'redirects.json');
244
+ const stainlessDir = path.join(dir.pathname, '_stainless');
245
+ mkdirSync(stainlessDir, { recursive: true });
246
+ const outputPath = path.join(stainlessDir, 'redirects.json');
165
247
  writeFileSync(outputPath, JSON.stringify(redirects, null, 2), {
166
248
  encoding: 'utf-8',
167
249
  });
@@ -171,7 +253,18 @@ function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroI
171
253
  };
172
254
  }
173
255
 
174
- export function stainlessDocs(config: StainlessDocsUserConfig) {
256
+ function sharedLoggerIntegration(): AstroIntegration {
257
+ return {
258
+ name: 'stainless',
259
+ hooks: {
260
+ 'astro:config:setup': ({ logger }) => {
261
+ setSharedLogger(logger);
262
+ },
263
+ },
264
+ };
265
+ }
266
+
267
+ export function stainlessDocs(config: StainlessDocsUserConfig): AstroIntegration[] {
175
268
  const normalizedConfigResult = parseStlDocsConfig(config);
176
269
  if (normalizedConfigResult.result === 'error') {
177
270
  // TODO: would be good to use the astro logger somehow
@@ -180,9 +273,34 @@ export function stainlessDocs(config: StainlessDocsUserConfig) {
180
273
  }
181
274
  const normalizedConfig = normalizedConfigResult.config;
182
275
 
276
+ // TODO: need to refactor this, but this allows us to get the base path for the API reference _if_ it exists
277
+ // if it doesn't exist, the value of basePath is null.
278
+ // the stl-starlight virtual module has base path, but it's not available when there's no API reference
279
+ const hasApiReference = normalizedConfig.apiReference !== null;
280
+ let apiReferenceBasePath: string | null = null;
281
+ if (hasApiReference) {
282
+ apiReferenceBasePath = normalizedConfig.apiReference?.basePath ?? '/api';
283
+ }
284
+
183
285
  return [
286
+ sharedLoggerIntegration(), // this **must** be first so it can set the shared logger used by our other integrations
184
287
  react(),
185
288
  stainlessDocsStarlightIntegration(normalizedConfig),
186
- stainlessDocsIntegration(normalizedConfig),
289
+ stainlessDocsIntegration(normalizedConfig, apiReferenceBasePath),
290
+ conditionalIntegration({
291
+ condition: !config.experimental?.disableProseMarkdownRendering,
292
+ integration: stainlessDocsMarkdownRenderer({ apiReferenceBasePath }),
293
+ reason: 'disabled by experimental config "disableProseMarkdownRendering"',
294
+ }),
295
+ conditionalIntegration({
296
+ condition: !config.experimental?.disableStainlessProseIndexing,
297
+ integration: stainlessDocsAlgoliaProseIndexing({ apiReferenceBasePath }),
298
+ reason: 'disabled by experimental config "disableStainlessProseIndexing"',
299
+ }),
300
+ conditionalIntegration({
301
+ condition: !config.experimental?.disableStainlessProseIndexing,
302
+ integration: stainlessDocsVectorProseIndexing(normalizedConfig, apiReferenceBasePath),
303
+ reason: 'disabled by experimental config "disableStainlessProseIndexing"',
304
+ }),
187
305
  ];
188
306
  }
@@ -1,8 +1,10 @@
1
1
  import type { StainlessStarlightUserConfig } from '../plugin/loadPluginConfig';
2
- import type { StarlightUserConfig } from '@astrojs/starlight/types';
2
+ import type { StarlightPlugin, StarlightUserConfig } from '@astrojs/starlight/types';
3
3
  import type { ButtonVariant } from '@stainless-api/ui-primitives';
4
4
  import type { AnchorHTMLAttributes } from 'react';
5
5
  import type starlight from '@astrojs/starlight';
6
+ import { normalizeFonts, type StlDocsFontConfig } from './fonts';
7
+ import type { ExamplePromptResponse } from './aiChatExamples';
6
8
 
7
9
  type StarlightConfig = Parameters<typeof starlight>[0];
8
10
 
@@ -32,20 +34,22 @@ type PassThroughStarlightConfigOptions = Pick<
32
34
  | 'lastUpdated'
33
35
  | 'pagination'
34
36
  | 'sidebar'
37
+ | 'expressiveCode'
35
38
  >;
36
39
 
37
40
  type ExperimentalStarlightCompatibilityConfig = Pick<
38
41
  StarlightConfigDefined,
39
- 'components' | 'routeMiddleware' | 'plugins'
42
+ 'components' | 'routeMiddleware' | 'plugins' | 'prerender' | 'pagefind'
40
43
  >;
41
44
 
42
- type Tabs = {
45
+ export type Tabs = {
43
46
  label: string;
44
47
  link: string;
45
48
  sidebar?: SidebarEntry[];
46
49
  /**
47
50
  * Whether to hide the tab in the tab bar.
48
- * Defaults to `false`.
51
+ *
52
+ * @default false
49
53
  */
50
54
  hidden?: boolean;
51
55
  }[];
@@ -64,10 +68,44 @@ export type StainlessDocsUserConfig = {
64
68
  layout?: HeaderLayout;
65
69
  links?: HeaderLink[];
66
70
  };
71
+ disableCredits?: boolean;
72
+ fonts?: StlDocsFontConfig;
67
73
  experimental?: {
68
74
  starlightCompat?: ExperimentalStarlightCompatibilityConfig;
69
75
  enableClientRouter?: boolean;
76
+ /**
77
+ * Disable markdown rendering for prose content. Only disable this if it is causing issues.
78
+ *
79
+ * @default false
80
+ */
81
+ disableProseMarkdownRendering?: boolean;
82
+ disableStainlessProseIndexing?: boolean;
83
+ aiChat?: { exampleOverrides?: ExamplePromptResponse } | true;
84
+ /**
85
+ * Whether to link group titles to overview pages. Note: overview pages must already be present in the sidebar for this to work.
86
+ *
87
+ * @default false
88
+ */
89
+ linkGroupTitlesToOverviewPages?: boolean;
70
90
  };
91
+ /**
92
+ * Whether to show the context menu with options like "Copy as Markdown" and "Open in ChatGPT".
93
+ *
94
+ * @default true
95
+ */
96
+ contextMenu?: boolean;
97
+
98
+ /**
99
+ * Whether to render page descriptions in prose page headers
100
+ *
101
+ * @default true
102
+ */
103
+ renderPageDescriptions?: boolean;
104
+ /**
105
+ * Stainless Docs plugins.
106
+ * Each plugin is a function that receives the normalized config and returns a Starlight plugin.
107
+ */
108
+ plugins?: ((config: Exclude<NormalizedStainlessDocsConfig, 'plugins'>) => StarlightPlugin)[];
71
109
  } & PassThroughStarlightConfigOptions;
72
110
 
73
111
  type HeaderLayout = 'default' | 'stacked';
@@ -99,6 +137,19 @@ function normalizeRouteMiddleware(userConfig: StainlessDocsUserConfig) {
99
137
  return entry;
100
138
  }
101
139
 
140
+ function normalizeSiteTitle(userConfig: StainlessDocsUserConfig) {
141
+ if (typeof userConfig.title === 'string') {
142
+ return userConfig.title;
143
+ }
144
+ if (typeof userConfig.title === 'object') {
145
+ const firstValue = Object.values(userConfig.title)[0];
146
+ if (typeof firstValue === 'string') {
147
+ return firstValue;
148
+ }
149
+ }
150
+ throw new Error('Site title provided in config is not valid.');
151
+ }
152
+
102
153
  function normalizeConfig(userConfig: StainlessDocsUserConfig) {
103
154
  const splitTabsEnabled = areSplitTabsEnabled(userConfig);
104
155
 
@@ -112,6 +163,7 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
112
163
  layout: userConfig.header?.layout ?? 'default',
113
164
  links: userConfig.header?.links ?? [],
114
165
  },
166
+ fonts: normalizeFonts(userConfig.fonts),
115
167
  starlightPassThrough: {
116
168
  tableOfContents: userConfig.tableOfContents,
117
169
  titleDelimiter: userConfig.titleDelimiter,
@@ -125,16 +177,29 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
125
177
  locales: userConfig.locales,
126
178
  lastUpdated: userConfig.lastUpdated,
127
179
  pagination: userConfig.pagination,
180
+ prerender: userConfig.experimental?.starlightCompat?.prerender ?? true,
128
181
  },
129
182
  starlightCompat: {
130
183
  components: userConfig.experimental?.starlightCompat?.components ?? {},
131
184
  plugins: userConfig.experimental?.starlightCompat?.plugins ?? [],
185
+ pagefind: userConfig.experimental?.starlightCompat?.pagefind ?? true,
132
186
  routeMiddleware: normalizeRouteMiddleware(userConfig),
133
187
  },
134
188
  enableClientRouter: userConfig.experimental?.enableClientRouter ?? false,
135
189
  apiReference: userConfig.apiReference ?? null,
136
190
  sidebar: userConfig.sidebar,
137
- includeAIDropdownOptions: userConfig.apiReference?.includeAIDropdownOptions ?? false,
191
+ enableStainlessProseIndexing:
192
+ userConfig.experimental?.disableStainlessProseIndexing === true ? false : true,
193
+ enableProseMarkdownRendering:
194
+ userConfig.experimental?.disableProseMarkdownRendering === true ? false : true,
195
+ contextMenu: userConfig.contextMenu ?? true,
196
+ expressiveCode: userConfig.expressiveCode,
197
+ renderPageDescriptions: userConfig.renderPageDescriptions ?? true,
198
+ plugins: userConfig.plugins ?? [],
199
+ aiChat: userConfig.experimental?.aiChat,
200
+ linkGroupTitlesToOverviewPages: userConfig.experimental?.linkGroupTitlesToOverviewPages ?? false,
201
+ credits: !userConfig.disableCredits,
202
+ siteTitle: normalizeSiteTitle(userConfig),
138
203
  };
139
204
 
140
205
  return configWithDefaults;
@@ -143,12 +208,12 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
143
208
  export type NormalizedStainlessDocsConfig = ReturnType<typeof normalizeConfig>;
144
209
 
145
210
  /*
146
- The goal of the code in this file is to take a user's config and normalize it.
211
+ The goal of the code in this file is to take a user's config and normalize it.
147
212
  Specifically: we want a single complete config format used throughout the internals of the plugin.
148
213
 
149
214
  We've tried to avoid any config values being optional/undefined. To accomplish this:
150
- - Any optional config values should have their defaults set here: eg. basePath defaults to /api
151
- - If a field is only used in certain contexts, we make each context a discriminated union (see SpecRetrieverConfig)
215
+ - Any optional config values should have their defaults set here: eg. basePath defaults to /api
216
+ - If a field is only used in certain contexts, we make each context a discriminated union (see SDKJSONInputs)
152
217
  - We prefer empty arrays over undefined/null
153
218
  */
154
219
  export function parseStlDocsConfig(userConfig: StainlessDocsUserConfig) {
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { batchBySize, MAX_BATCH_BYTES, MAX_BATCH_DOCS } from './proseDocSync';
3
+
4
+ function makeDoc(docId: string, sizeBytes: number) {
5
+ return {
6
+ docId,
7
+ content: Buffer.alloc(sizeBytes, 'x'),
8
+ sha256: 'fake',
9
+ source: `/${docId}`,
10
+ };
11
+ }
12
+
13
+ describe('batchBySize', () => {
14
+ it('returns empty array for no docs', () => {
15
+ expect(batchBySize([])).toEqual([]);
16
+ });
17
+
18
+ it('puts all docs in one batch when under both limits', () => {
19
+ const docs = Array.from({ length: 5 }, (_, i) => makeDoc(`doc-${i}`, 100));
20
+ const batches = batchBySize(docs);
21
+ expect(batches).toHaveLength(1);
22
+ expect(batches[0]).toHaveLength(5);
23
+ });
24
+
25
+ it('splits at MAX_BATCH_DOCS', () => {
26
+ const docs = Array.from({ length: MAX_BATCH_DOCS + 10 }, (_, i) => makeDoc(`doc-${i}`, 10));
27
+ const batches = batchBySize(docs);
28
+ expect(batches).toHaveLength(2);
29
+ expect(batches[0]).toHaveLength(MAX_BATCH_DOCS);
30
+ expect(batches[1]).toHaveLength(10);
31
+ });
32
+
33
+ it('splits when cumulative size exceeds MAX_BATCH_BYTES', () => {
34
+ const docSize = 10 * 1024 * 1024; // 10MB each
35
+ // 4 docs = 40MB > 30MB limit, so should split into [3, 1]
36
+ const docs = Array.from({ length: 4 }, (_, i) => makeDoc(`doc-${i}`, docSize));
37
+ const batches = batchBySize(docs);
38
+ expect(batches).toHaveLength(2);
39
+ expect(batches[0]).toHaveLength(3);
40
+ expect(batches[1]).toHaveLength(1);
41
+ });
42
+
43
+ it('handles a single doc larger than MAX_BATCH_BYTES', () => {
44
+ const docs = [makeDoc('huge', MAX_BATCH_BYTES + 1)];
45
+ const batches = batchBySize(docs);
46
+ // Still goes into a batch on its own — we can't split a single doc
47
+ expect(batches).toHaveLength(1);
48
+ expect(batches[0]).toHaveLength(1);
49
+ });
50
+
51
+ it('byte limit takes precedence over doc count when hit first', () => {
52
+ // 2 docs of 20MB each = 40MB > 30MB limit, well under 100 doc limit
53
+ const docs = [makeDoc('a', 20 * 1024 * 1024), makeDoc('b', 20 * 1024 * 1024)];
54
+ const batches = batchBySize(docs);
55
+ expect(batches).toHaveLength(2);
56
+ expect(batches[0]).toHaveLength(1);
57
+ expect(batches[1]).toHaveLength(1);
58
+ });
59
+
60
+ it('creates multiple batches for many large docs', () => {
61
+ const docSize = 8 * 1024 * 1024; // 8MB each
62
+ // 10 docs * 8MB = 80MB, should split into batches of ~3 (24MB each)
63
+ const docs = Array.from({ length: 10 }, (_, i) => makeDoc(`doc-${i}`, docSize));
64
+ const batches = batchBySize(docs);
65
+ // Every batch should be <= MAX_BATCH_BYTES
66
+ for (const batch of batches) {
67
+ const totalBytes = batch.reduce((sum, d) => sum + d.content.byteLength, 0);
68
+ expect(totalBytes).toBeLessThanOrEqual(MAX_BATCH_BYTES);
69
+ }
70
+ // All docs should be accounted for
71
+ const totalDocs = batches.reduce((sum, b) => sum + b.length, 0);
72
+ expect(totalDocs).toBe(10);
73
+ });
74
+ });