@stainless-api/docs 0.1.0-beta.9 → 0.1.0-beta.90

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 (142) hide show
  1. package/CHANGELOG.md +748 -0
  2. package/eslint-suppressions.json +32 -0
  3. package/locals.d.ts +17 -0
  4. package/package.json +49 -40
  5. package/playground-virtual-modules.d.ts +96 -0
  6. package/plugin/assets/languages/cli.svg +14 -0
  7. package/plugin/assets/languages/csharp.svg +1 -0
  8. package/plugin/buildAlgoliaIndex.ts +38 -11
  9. package/plugin/components/MethodDescription.tsx +54 -0
  10. package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
  11. package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
  12. package/plugin/components/RequestBuilder/index.tsx +31 -0
  13. package/plugin/components/RequestBuilder/props.ts +9 -0
  14. package/plugin/components/RequestBuilder/spec-helpers.ts +50 -0
  15. package/plugin/components/RequestBuilder/styles.css +67 -0
  16. package/plugin/components/SDKSelect.astro +20 -104
  17. package/plugin/components/SnippetCode.tsx +111 -66
  18. package/plugin/components/StainlessIslands.tsx +126 -0
  19. package/plugin/components/search/SearchAlgolia.astro +45 -28
  20. package/plugin/components/search/SearchIsland.tsx +47 -29
  21. package/plugin/generateAPIReferenceLink.ts +2 -2
  22. package/plugin/globalJs/ai-dropdown-options.ts +243 -0
  23. package/plugin/globalJs/code-snippets.ts +15 -8
  24. package/plugin/globalJs/copy.ts +94 -17
  25. package/plugin/globalJs/create-playground.shim.ts +3 -0
  26. package/plugin/globalJs/method-descriptions.ts +33 -0
  27. package/plugin/globalJs/navigation.ts +10 -29
  28. package/plugin/globalJs/playground-data.shim.ts +1 -0
  29. package/plugin/globalJs/playground-data.ts +14 -0
  30. package/plugin/helpers/generateDocsRoutes.ts +27 -0
  31. package/plugin/helpers/getDocsLanguages.ts +9 -0
  32. package/plugin/index.ts +292 -116
  33. package/plugin/languages.ts +7 -2
  34. package/plugin/loadPluginConfig.ts +155 -79
  35. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +1 -1
  36. package/plugin/react/Routing.tsx +204 -132
  37. package/plugin/referencePlaceholderUtils.ts +18 -15
  38. package/plugin/replaceSidebarPlaceholderMiddleware.ts +38 -34
  39. package/plugin/routes/Docs.astro +65 -117
  40. package/plugin/routes/DocsStatic.astro +7 -4
  41. package/plugin/routes/Overview.astro +20 -24
  42. package/plugin/routes/markdown.ts +12 -11
  43. package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +30 -54
  44. package/plugin/specs/fetchSpecSSR.ts +21 -0
  45. package/plugin/specs/generateSpec.ts +50 -0
  46. package/plugin/specs/index.ts +238 -0
  47. package/plugin/{cms → specs}/worker.ts +82 -5
  48. package/plugin/vendor/preview.worker.docs.js +20928 -17830
  49. package/plugin/vendor/templates/go.md +1 -1
  50. package/plugin/vendor/templates/python.md +1 -1
  51. package/resolveSrcFile.ts +10 -0
  52. package/scripts/vendor_deps.ts +5 -5
  53. package/shared/getProsePages.ts +42 -0
  54. package/shared/getSharedLogger.ts +15 -0
  55. package/shared/terminalUtils.ts +3 -0
  56. package/shared/virtualModule.ts +54 -1
  57. package/src/content.config.ts +9 -0
  58. package/stl-docs/components/AIDropdown.tsx +63 -0
  59. package/stl-docs/components/AiChatIsland.tsx +14 -0
  60. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
  61. package/stl-docs/components/Head.astro +20 -0
  62. package/stl-docs/components/Header.astro +6 -8
  63. package/stl-docs/components/PageFrame.astro +18 -0
  64. package/stl-docs/components/PageTitle.astro +82 -0
  65. package/stl-docs/components/TableOfContents.astro +34 -0
  66. package/stl-docs/components/ThemeProvider.astro +36 -0
  67. package/stl-docs/components/ThemeSelect.astro +84 -139
  68. package/stl-docs/components/content-panel/ContentPanel.astro +16 -46
  69. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +17 -1
  70. package/stl-docs/components/headers/StackedHeader.astro +29 -24
  71. package/stl-docs/components/icons/chat-gpt.tsx +2 -2
  72. package/stl-docs/components/icons/cursor.tsx +10 -0
  73. package/stl-docs/components/icons/gemini.tsx +19 -0
  74. package/stl-docs/components/icons/markdown.tsx +1 -1
  75. package/stl-docs/components/index.ts +1 -0
  76. package/stl-docs/components/mintlify-compat/Accordion.astro +7 -5
  77. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +7 -3
  78. package/stl-docs/components/mintlify-compat/Columns.astro +40 -42
  79. package/stl-docs/components/mintlify-compat/Frame.astro +16 -18
  80. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +1 -1
  81. package/stl-docs/components/mintlify-compat/callouts/Check.astro +1 -1
  82. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +1 -1
  83. package/stl-docs/components/mintlify-compat/callouts/Info.astro +1 -1
  84. package/stl-docs/components/mintlify-compat/callouts/Note.astro +1 -1
  85. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +1 -1
  86. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +1 -1
  87. package/stl-docs/components/mintlify-compat/card.css +33 -35
  88. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  89. package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -70
  90. package/stl-docs/components/nav-tabs/NavTabs.astro +78 -80
  91. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -8
  92. package/stl-docs/components/nav-tabs/buildNavLinks.ts +3 -2
  93. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  94. package/stl-docs/components/pagination/Pagination.astro +175 -0
  95. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  96. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  97. package/stl-docs/components/pagination/util.ts +71 -0
  98. package/stl-docs/components/scripts.ts +1 -0
  99. package/stl-docs/components/sidebars/BaseSidebar.astro +9 -2
  100. package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
  101. package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
  102. package/stl-docs/disableCalloutSyntax.ts +36 -0
  103. package/stl-docs/fonts.ts +186 -0
  104. package/stl-docs/index.ts +153 -50
  105. package/stl-docs/loadStlDocsConfig.ts +51 -7
  106. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +61 -0
  107. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
  108. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  109. package/stl-docs/proseSearchIndexing.ts +606 -0
  110. package/stl-docs/tabsMiddleware.ts +13 -4
  111. package/styles/code.css +128 -136
  112. package/styles/links.css +11 -48
  113. package/styles/method-descriptions.css +36 -0
  114. package/styles/overrides.css +49 -57
  115. package/styles/page.css +100 -59
  116. package/styles/sdk_select.css +9 -7
  117. package/styles/search.css +57 -69
  118. package/styles/sidebar.css +26 -156
  119. package/styles/{variables.css → sl-variables.css} +3 -2
  120. package/styles/stldocs-variables.css +6 -0
  121. package/styles/toc.css +41 -34
  122. package/theme.css +11 -11
  123. package/tsconfig.json +2 -5
  124. package/virtual-module.d.ts +47 -7
  125. package/components/variables.css +0 -135
  126. package/plugin/cms/client.ts +0 -62
  127. package/plugin/cms/server.ts +0 -268
  128. package/plugin/globalJs/ai-dropdown.ts +0 -57
  129. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
  130. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
  131. package/stl-docs/components/mintlify-compat/Step.astro +0 -58
  132. package/stl-docs/components/mintlify-compat/Steps.astro +0 -17
  133. package/styles/fonts.css +0 -68
  134. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  135. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  136. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  137. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  138. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  139. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  140. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  141. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  142. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
package/stl-docs/index.ts CHANGED
@@ -1,6 +1,7 @@
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
 
@@ -16,11 +17,18 @@ 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 { setSharedLogger } from '../shared/getSharedLogger';
24
+ import { stainlessDocsAlgoliaProseIndexing, stainlessDocsVectorProseIndexing } from './proseSearchIndexing';
25
+ import { stainlessStarlight } from '../plugin';
26
+ import { getFontRoles, flattenFonts } from './fonts';
21
27
 
22
28
  export * from '../plugin';
23
29
 
30
+ const COMPONENTS_FOLDER = '/stl-docs/components';
31
+
24
32
  function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig) {
25
33
  // We transform our tabs into a Starlight sidebar
26
34
  // This gives them all the built-in features of Starlight (eg. auto-generated entries by directory)
@@ -40,21 +48,44 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
40
48
  }
41
49
 
42
50
  type ComponentOverrides = StarlightConfigDefined['components'];
43
- const plugins = [...config.starlightCompat.plugins];
44
-
45
51
  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',
52
+ PageFrame: resolveSrcFile(COMPONENTS_FOLDER, './PageFrame.astro'),
53
+
54
+ Head: resolveSrcFile(COMPONENTS_FOLDER, './Head.astro'),
55
+ Header: resolveSrcFile(COMPONENTS_FOLDER, './Header.astro'),
56
+ ThemeProvider: resolveSrcFile(COMPONENTS_FOLDER, './ThemeProvider.astro'),
57
+ ThemeSelect: resolveSrcFile(COMPONENTS_FOLDER, './ThemeSelect.astro'),
58
+
59
+ Sidebar: resolveSrcFile(COMPONENTS_FOLDER, './sidebars/BaseSidebar.astro'),
60
+ ContentPanel: resolveSrcFile(COMPONENTS_FOLDER, './content-panel/ContentPanel.astro'),
61
+ TableOfContents: resolveSrcFile(COMPONENTS_FOLDER, './TableOfContents.astro'),
62
+
63
+ PageTitle: resolveSrcFile(COMPONENTS_FOLDER, './PageTitle.astro'),
64
+ Pagination: resolveSrcFile(COMPONENTS_FOLDER, './pagination/Pagination.astro'),
50
65
  };
51
66
 
67
+ const plugins: StarlightPlugin[] = [
68
+ // Disable starlight callout syntax in favor of our own component
69
+ disableCalloutSyntaxStarlightPlugin,
70
+ ];
71
+
52
72
  if (config.apiReference !== null) {
53
- componentOverrides.Sidebar = '@stainless-api/docs/SDKSelectSidebar';
54
- componentOverrides.Search = '@stainless-api/docs/Search';
55
- plugins.unshift(stainlessStarlight(config.apiReference));
73
+ plugins.push(
74
+ stainlessStarlight({
75
+ ...config.apiReference,
76
+ contextMenu: config.contextMenu,
77
+ experimentalPrerender:
78
+ config.apiReference.experimentalPrerender === undefined
79
+ ? config.starlightPassThrough.prerender
80
+ : config.apiReference.experimentalPrerender,
81
+ }),
82
+ );
83
+ componentOverrides.Sidebar = resolveSrcFile(COMPONENTS_FOLDER, './sidebars/SDKSelectSidebar.astro');
84
+ componentOverrides.Search = resolveSrcFile('/plugin/components/search/Search.astro');
56
85
  }
57
86
 
87
+ plugins.push(...config.starlightCompat.plugins, ...config.plugins.map((p) => p(config)));
88
+
58
89
  // TODO: re-add once we figure out what to do with the client router
59
90
  // if (config.enableClientRouter) {
60
91
  // // logger.info(`Client router is enabled`);
@@ -63,6 +94,8 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
63
94
  // // logger.info(`Client router is disabled`);
64
95
  // }
65
96
 
97
+ const userExpressiveCode = typeof config.expressiveCode === 'object' ? config.expressiveCode : {};
98
+
66
99
  return starlight({
67
100
  ...config.starlightPassThrough,
68
101
  sidebar,
@@ -86,68 +119,111 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
86
119
  setupNavLinksInitial();
87
120
  `,
88
121
  },
89
- // TODO: for users who are overriding the font stack in their own styles, how can we know that
90
- // and preload their font instead of ours?
91
- {
92
- tag: 'link',
93
- attrs: {
94
- rel: 'preload',
95
- as: 'font',
96
- type: 'font/woff2',
97
- crossorigin: 'anonymous',
98
- href: geistPath,
122
+ ],
123
+ routeMiddleware: [
124
+ ...config.starlightCompat.routeMiddleware,
125
+ resolveSrcFile('/stl-docs/tabsMiddleware.ts'),
126
+ ],
127
+ customCss: [resolveSrcFile('/theme.css'), ...config.customCss],
128
+
129
+ expressiveCode: {
130
+ ...userExpressiveCode,
131
+ themes: userExpressiveCode.themes ?? ['github-light', 'github-dark'],
132
+ styleOverrides: {
133
+ ...userExpressiveCode.styleOverrides,
134
+ textMarkers: {
135
+ insBackground: 'var(--stl-color-green-muted-background)',
136
+ insBorderColor: 'var(--stl-color-green-border)',
137
+ insDiffIndicatorColor: 'var(--stl-color-green-foreground-reduced)',
138
+
139
+ delBackground: 'var(--stl-color-red-muted-background)',
140
+ delBorderColor: 'var(--stl-color-red-border)',
141
+ delDiffIndicatorColor: 'var(--stl-color-red-foreground-reduced)',
142
+
143
+ markBackground: 'var(--stl-color-blue-muted-background)',
144
+ markBorderColor: 'var(--stl-color-blue-border)',
145
+ ...userExpressiveCode.styleOverrides?.textMarkers,
99
146
  },
100
147
  },
101
- ],
102
- routeMiddleware: [...config.starlightCompat.routeMiddleware, '@stainless-api/docs/tabsMiddleware'],
103
- customCss: ['@stainless-api/docs/theme', ...config.customCss],
148
+ },
104
149
  plugins,
105
150
  });
106
151
  }
107
152
 
108
- function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroIntegration {
109
- const virtualId = `virtual:stl-docs-virtual-module`;
153
+ function stainlessDocsIntegration(
154
+ config: NormalizedStainlessDocsConfig,
155
+ apiReferenceBasePath: string | null,
156
+ ): AstroIntegration {
110
157
  // The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
111
- const resolvedId = `\0${virtualId}`;
158
+ const resolveVirtualModuleId = (id: string) => `\0${id}`;
112
159
  let redirects: NormalizedRedirectConfig | null = null;
113
160
 
114
161
  return {
115
- name: 'stl-docs-integration',
162
+ name: 'stl-docs-astro',
116
163
  hooks: {
117
164
  'astro:config:setup': ({ updateConfig, command, config: astroConfig }) => {
118
- // // we only handle redirects for builds
119
- // // in dev, Astro handles them for us
165
+ // we only handle redirects for builds
166
+ // in dev, Astro handles them for us
120
167
  if (command === 'build' && astroConfig.redirects) {
121
168
  redirects = normalizeRedirects(astroConfig.redirects);
122
169
  }
123
170
 
171
+ const virtualModules = new Map(
172
+ Object.entries({
173
+ 'virtual:stl-docs-virtual-module': buildVirtualModuleString({
174
+ TABS: config.tabs,
175
+ SPLIT_TABS_ENABLED: config.splitTabsEnabled,
176
+ HEADER_LINKS: config.header.links,
177
+ HEADER_LAYOUT: config.header.layout,
178
+ ENABLE_CLIENT_ROUTER: config.enableClientRouter,
179
+ API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
180
+ ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
181
+ ENABLE_CONTEXT_MENU: config.contextMenu, // TODO: do not duplicate this between both virtual modules
182
+ RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
183
+ FONTS: getFontRoles(config.fonts),
184
+ } satisfies typeof StlDocsVirtualModule),
185
+
186
+ 'virtual:stl-docs/components/AiChat.tsx': `
187
+ ${
188
+ config.aiChat
189
+ ? `export { default } from ${JSON.stringify(config.aiChat.chatComponentPath)};`
190
+ : // export null when no AI chat component is provided
191
+ `export default null;`
192
+ }
193
+ export const STAINLESS_PROJECT = ${config.apiReference ? JSON.stringify(config.apiReference.stainlessProject) : 'undefined'};
194
+ `,
195
+ }),
196
+ );
197
+
124
198
  updateConfig({
199
+ experimental: {
200
+ fonts: [...flattenFonts(config.fonts), ...(astroConfig.experimental?.fonts ?? [])],
201
+ },
125
202
  vite: {
126
- ssr: {
127
- noExternal: ['@stainless-api/ui-primitives'],
128
- },
129
- optimizeDeps: { include: ['@stainless-api/ui-primitives'] },
130
203
  plugins: [
131
204
  {
132
205
  name: 'stl-docs-vite',
133
206
  resolveId(id) {
134
- if (id === virtualId) {
135
- return resolvedId;
136
- }
207
+ if (virtualModules.has(id)) return resolveVirtualModuleId(id);
137
208
  },
138
209
  load(id) {
139
- if (id === resolvedId) {
140
- return buildVirtualModuleString({
141
- TABS: config.tabs,
142
- SPLIT_TABS_ENABLED: config.splitTabsEnabled,
143
- HEADER_LINKS: config.header.links,
144
- HEADER_LAYOUT: config.header.layout,
145
- ENABLE_CLIENT_ROUTER: config.enableClientRouter,
146
- });
147
- }
210
+ const bare = id.replace(/^\0/, '');
211
+ if (virtualModules.has(bare)) return virtualModules.get(bare);
148
212
  },
149
213
  },
150
214
  ],
215
+ optimizeDeps: {
216
+ include: config.aiChat
217
+ ? [
218
+ '@stainless-api/docs-ai-chat > @stainless-api/ai-chat > lucide-react',
219
+ '@stainless-api/docs-ai-chat > @stainless-api/ai-chat > motion',
220
+ '@stainless-api/docs-ai-chat > @stainless-api/ai-chat > motion > framer-motion',
221
+ '@stainless-api/docs-ai-chat > @stainless-api/ai-chat > react-markdown',
222
+ '@stainless-api/docs-ai-chat > @stainless-api/ai-chat > react-syntax-highlighter',
223
+ '@stainless-api/docs-ai-chat > @stainless-api/ai-chat > remark-gfm',
224
+ ]
225
+ : [],
226
+ },
151
227
  },
152
228
  build: {
153
229
  ...astroConfig.build,
@@ -158,7 +234,7 @@ function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroI
158
234
  'astro:build:done': ({ dir }) => {
159
235
  if (redirects !== null) {
160
236
  const stainlessDir = join(dir.pathname, '_stainless');
161
- mkdirSync(stainlessDir);
237
+ mkdirSync(stainlessDir, { recursive: true });
162
238
  const outputPath = join(stainlessDir, 'redirects.json');
163
239
  writeFileSync(outputPath, JSON.stringify(redirects, null, 2), {
164
240
  encoding: 'utf-8',
@@ -169,7 +245,18 @@ function stainlessDocsIntegration(config: NormalizedStainlessDocsConfig): AstroI
169
245
  };
170
246
  }
171
247
 
172
- export function stainlessDocs(config: StainlessDocsUserConfig) {
248
+ function sharedLoggerIntegration(): AstroIntegration {
249
+ return {
250
+ name: 'stainless',
251
+ hooks: {
252
+ 'astro:config:setup': ({ logger }) => {
253
+ setSharedLogger(logger);
254
+ },
255
+ },
256
+ };
257
+ }
258
+
259
+ export function stainlessDocs(config: StainlessDocsUserConfig): StarlightPlugin[] {
173
260
  const normalizedConfigResult = parseStlDocsConfig(config);
174
261
  if (normalizedConfigResult.result === 'error') {
175
262
  // TODO: would be good to use the astro logger somehow
@@ -178,9 +265,25 @@ export function stainlessDocs(config: StainlessDocsUserConfig) {
178
265
  }
179
266
  const normalizedConfig = normalizedConfigResult.config;
180
267
 
268
+ // TODO: need to refactor this, but this allows us to get the base path for the API reference _if_ it exists
269
+ // if it doesn't exist, the value of basePath is null.
270
+ // the stl-starlight virtual module has base path, but it's not available when there's no API reference
271
+ const hasApiReference = normalizedConfig.apiReference !== null;
272
+ let apiReferenceBasePath: string | null = null;
273
+ if (hasApiReference) {
274
+ apiReferenceBasePath = normalizedConfig.apiReference?.basePath ?? '/api';
275
+ }
276
+
181
277
  return [
278
+ sharedLoggerIntegration(), // this **must** be first so it can set the shared logger used by our other integrations
182
279
  react(),
183
280
  stainlessDocsStarlightIntegration(normalizedConfig),
184
- stainlessDocsIntegration(normalizedConfig),
281
+ stainlessDocsIntegration(normalizedConfig, apiReferenceBasePath),
282
+ stainlessDocsMarkdownRenderer({
283
+ enabled: normalizedConfig.enableProseMarkdownRendering,
284
+ apiReferenceBasePath,
285
+ }),
286
+ stainlessDocsAlgoliaProseIndexing({ apiReferenceBasePath }),
287
+ stainlessDocsVectorProseIndexing(normalizedConfig, apiReferenceBasePath),
185
288
  ];
186
289
  }
@@ -1,8 +1,9 @@
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';
6
7
 
7
8
  type StarlightConfig = Parameters<typeof starlight>[0];
8
9
 
@@ -32,20 +33,22 @@ type PassThroughStarlightConfigOptions = Pick<
32
33
  | 'lastUpdated'
33
34
  | 'pagination'
34
35
  | 'sidebar'
36
+ | 'expressiveCode'
35
37
  >;
36
38
 
37
39
  type ExperimentalStarlightCompatibilityConfig = Pick<
38
40
  StarlightConfigDefined,
39
- 'components' | 'routeMiddleware' | 'plugins'
41
+ 'components' | 'routeMiddleware' | 'plugins' | 'prerender'
40
42
  >;
41
43
 
42
- type Tabs = {
44
+ export type Tabs = {
43
45
  label: string;
44
46
  link: string;
45
47
  sidebar?: SidebarEntry[];
46
48
  /**
47
49
  * Whether to hide the tab in the tab bar.
48
- * Defaults to `false`.
50
+ *
51
+ * @default false
49
52
  */
50
53
  hidden?: boolean;
51
54
  }[];
@@ -64,10 +67,36 @@ export type StainlessDocsUserConfig = {
64
67
  layout?: HeaderLayout;
65
68
  links?: HeaderLink[];
66
69
  };
70
+ fonts?: StlDocsFontConfig;
67
71
  experimental?: {
68
72
  starlightCompat?: ExperimentalStarlightCompatibilityConfig;
69
73
  enableClientRouter?: boolean;
74
+ /**
75
+ * Disable markdown rendering for prose content. Only disable this if it is causing issues.
76
+ *
77
+ * @default false
78
+ */
79
+ disableProseMarkdownRendering?: boolean;
80
+ aiChat?: { chatComponentPath: string };
70
81
  };
82
+ /**
83
+ * Whether to show the context menu with options like "Copy as Markdown" and "Open in ChatGPT".
84
+ *
85
+ * @default true
86
+ */
87
+ contextMenu?: boolean;
88
+
89
+ /**
90
+ * Whether to render page descriptions in prose page headers
91
+ *
92
+ * @default true
93
+ */
94
+ renderPageDescriptions?: boolean;
95
+ /**
96
+ * Stainless Docs plugins.
97
+ * Each plugin is a function that receives the normalized config and returns a Starlight plugin.
98
+ */
99
+ plugins?: ((config: Exclude<NormalizedStainlessDocsConfig, 'plugins'>) => StarlightPlugin)[];
71
100
  } & PassThroughStarlightConfigOptions;
72
101
 
73
102
  type HeaderLayout = 'default' | 'stacked';
@@ -112,6 +141,7 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
112
141
  layout: userConfig.header?.layout ?? 'default',
113
142
  links: userConfig.header?.links ?? [],
114
143
  },
144
+ fonts: normalizeFonts(userConfig.fonts),
115
145
  starlightPassThrough: {
116
146
  tableOfContents: userConfig.tableOfContents,
117
147
  titleDelimiter: userConfig.titleDelimiter,
@@ -119,6 +149,13 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
119
149
  description: userConfig.description,
120
150
  tagline: userConfig.tagline,
121
151
  logo: userConfig.logo,
152
+ favicon: userConfig.favicon,
153
+ disable404Route: userConfig.disable404Route,
154
+ editLink: userConfig.editLink,
155
+ locales: userConfig.locales,
156
+ lastUpdated: userConfig.lastUpdated,
157
+ pagination: userConfig.pagination,
158
+ prerender: userConfig.experimental?.starlightCompat?.prerender ?? true,
122
159
  },
123
160
  starlightCompat: {
124
161
  components: userConfig.experimental?.starlightCompat?.components ?? {},
@@ -128,6 +165,13 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
128
165
  enableClientRouter: userConfig.experimental?.enableClientRouter ?? false,
129
166
  apiReference: userConfig.apiReference ?? null,
130
167
  sidebar: userConfig.sidebar,
168
+ enableProseMarkdownRendering:
169
+ userConfig.experimental?.disableProseMarkdownRendering === true ? false : true,
170
+ contextMenu: userConfig.contextMenu ?? true,
171
+ expressiveCode: userConfig.expressiveCode,
172
+ renderPageDescriptions: userConfig.renderPageDescriptions ?? true,
173
+ plugins: userConfig.plugins ?? [],
174
+ aiChat: userConfig.experimental?.aiChat,
131
175
  };
132
176
 
133
177
  return configWithDefaults;
@@ -136,12 +180,12 @@ function normalizeConfig(userConfig: StainlessDocsUserConfig) {
136
180
  export type NormalizedStainlessDocsConfig = ReturnType<typeof normalizeConfig>;
137
181
 
138
182
  /*
139
- The goal of the code in this file is to take a user's config and normalize it.
183
+ The goal of the code in this file is to take a user's config and normalize it.
140
184
  Specifically: we want a single complete config format used throughout the internals of the plugin.
141
185
 
142
186
  We've tried to avoid any config values being optional/undefined. To accomplish this:
143
- - Any optional config values should have their defaults set here: eg. basePath defaults to /api
144
- - If a field is only used in certain contexts, we make each context a discriminated union (see SpecRetrieverConfig)
187
+ - Any optional config values should have their defaults set here: eg. basePath defaults to /api
188
+ - If a field is only used in certain contexts, we make each context a discriminated union (see SDKJSONInputs)
145
189
  - We prefer empty arrays over undefined/null
146
190
  */
147
191
  export function parseStlDocsConfig(userConfig: StainlessDocsUserConfig) {
@@ -0,0 +1,61 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import { readFile, writeFile } from 'fs/promises';
3
+ import { toMarkdown } from './toMarkdown';
4
+ import { resolveSrcFile } from '../../resolveSrcFile';
5
+ import { getSharedLogger } from '../../shared/getSharedLogger';
6
+ import { bold } from '../../shared/terminalUtils';
7
+ import { getProsePages } from '../../shared/getProsePages';
8
+
9
+ export function stainlessDocsMarkdownRenderer({
10
+ enabled,
11
+ apiReferenceBasePath,
12
+ }: {
13
+ enabled: boolean;
14
+ apiReferenceBasePath: string | null;
15
+ }): AstroIntegration {
16
+ return {
17
+ name: 'stl-docs-md',
18
+ hooks: {
19
+ 'astro:config:setup': ({ addMiddleware }) => {
20
+ if (enabled) {
21
+ addMiddleware({
22
+ entrypoint: resolveSrcFile('/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts'),
23
+ order: 'post',
24
+ });
25
+ }
26
+ },
27
+ 'astro:build:done': async ({ logger: localLogger, dir }) => {
28
+ const logger = getSharedLogger({ fallback: localLogger });
29
+ if (!enabled) {
30
+ logger.info('Stainless Docs prose Markdown rendering is disabled, skipping...');
31
+ return;
32
+ }
33
+ const outputBasePath = dir.pathname;
34
+ const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
35
+
36
+ logger.info(bold(`Building ${pagesToRender.length} Markdown pages for prose content`));
37
+
38
+ let completedCount = 0;
39
+
40
+ for (const absHtmlPath of pagesToRender) {
41
+ const txt = await readFile(absHtmlPath, 'utf-8');
42
+ const md = await toMarkdown(txt);
43
+ if (md) {
44
+ const absMdPath = absHtmlPath.replace('.html', '.md');
45
+ await writeFile(absMdPath, md, 'utf-8');
46
+
47
+ completedCount++;
48
+
49
+ const relHtmlPath = absHtmlPath.replace(outputBasePath, '');
50
+ const relMdPath = absMdPath.replace(outputBasePath, '');
51
+
52
+ logger.info(`(${completedCount}/${pagesToRender.length}) ${relHtmlPath} -> ${relMdPath}`);
53
+ } else {
54
+ logger.error(`Failed to render ${absHtmlPath} as Markdown`);
55
+ process.exit(1);
56
+ }
57
+ }
58
+ },
59
+ },
60
+ };
61
+ }
@@ -0,0 +1,41 @@
1
+ import { defineMiddleware } from 'astro:middleware';
2
+ import { toMarkdown } from './toMarkdown';
3
+ import { API_REFERENCE_BASE_PATH } from 'virtual:stl-docs-virtual-module';
4
+ import path from 'path';
5
+
6
+ // this is only run in `astro dev` for rendering prose content as Markdown on the fly.
7
+ export const onRequest = defineMiddleware(async (context, next) => {
8
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
9
+ if (!import.meta.env.DEV) {
10
+ return next();
11
+ }
12
+
13
+ const resolvedBasePath = path.posix.join(import.meta.env.BASE_URL ?? '', API_REFERENCE_BASE_PATH);
14
+ if (resolvedBasePath && context.url.pathname.startsWith(resolvedBasePath)) {
15
+ // handled by the API reference API route in stl-starlight plugin
16
+ return next();
17
+ }
18
+
19
+ if (!context.url.pathname.endsWith('/index.md')) {
20
+ return next();
21
+ }
22
+
23
+ const pathname = context.url.pathname.replace('index.md', '');
24
+
25
+ // We must trim the trailing slash to support astro configs with `trailingSlash: 'never'`
26
+ const cleanPathname = pathname !== '/' ? pathname.replace(/\/$/, '') : pathname;
27
+ const htmlUrl = new URL(cleanPathname, context.url);
28
+
29
+ const resp = await fetch(htmlUrl);
30
+ if (!resp.ok) {
31
+ return new Response('Failed to fetch HTML', { status: 400 });
32
+ }
33
+ const html = await resp.text();
34
+ const md = await toMarkdown(html);
35
+
36
+ if (!md) {
37
+ return new Response('Failed to render Markdown', { status: 400 });
38
+ }
39
+
40
+ return new Response(md, { status: 200 });
41
+ });
@@ -0,0 +1,158 @@
1
+ import { unified } from 'unified';
2
+ import rehypeParse from 'rehype-parse';
3
+ import rehypeRemark from 'rehype-remark';
4
+ import remarkGfm from 'remark-gfm';
5
+ import remarkStringify from 'remark-stringify';
6
+ import { HTMLElement, parse } from 'node-html-parser';
7
+
8
+ type PaginationLink = {
9
+ href: string;
10
+ label: string;
11
+ };
12
+
13
+ type PaginationItems = {
14
+ prev: PaginationLink | null;
15
+ next: PaginationLink | null;
16
+ };
17
+
18
+ function parsePaginationLink(footer: HTMLElement, rel: 'next' | 'prev'): PaginationLink | null {
19
+ const link = footer.querySelector(`.pagination-links a[rel="${rel}"]`);
20
+ if (!link) {
21
+ return null;
22
+ }
23
+
24
+ const title = link.querySelector('.link-title');
25
+ if (!title) {
26
+ return null;
27
+ }
28
+
29
+ const href = link.getAttribute('href');
30
+ if (!href) {
31
+ return null;
32
+ }
33
+
34
+ return {
35
+ href,
36
+ label: title.text,
37
+ };
38
+ }
39
+
40
+ function isRelativeLink(href: string) {
41
+ return href.startsWith('/');
42
+ }
43
+
44
+ function hasExtension(href: string) {
45
+ return href.includes('.');
46
+ }
47
+
48
+ function removeTrailingSlash(href: string) {
49
+ return href.replace(/\/$/, '');
50
+ }
51
+
52
+ function makeMarkdownLinks(el: HTMLElement) {
53
+ el.querySelectorAll('a').forEach((a) => {
54
+ const href = a.getAttribute('href');
55
+ if (!href) {
56
+ return a;
57
+ }
58
+
59
+ if (isRelativeLink(href) && !hasExtension(href)) {
60
+ if (href === '/') {
61
+ a.setAttribute('href', '/index.md');
62
+ } else {
63
+ a.setAttribute('href', `${removeTrailingSlash(href)}/index.md`);
64
+ }
65
+ }
66
+ return a;
67
+ });
68
+ }
69
+
70
+ function removeHiddenElements(el: HTMLElement) {
71
+ const hiddenSelectors = ['.sl-anchor-link'];
72
+ for (const selector of hiddenSelectors) {
73
+ const hiddenElements = el.querySelectorAll(selector);
74
+ for (const hiddenElement of hiddenElements) {
75
+ hiddenElement.remove();
76
+ }
77
+ }
78
+ }
79
+
80
+ export async function toMarkdown(html: string) {
81
+ const root = parse(html);
82
+
83
+ const mainEl = root.querySelector('main');
84
+
85
+ if (!mainEl) {
86
+ return null;
87
+ }
88
+
89
+ makeMarkdownLinks(mainEl);
90
+
91
+ const footer = mainEl.querySelector('footer');
92
+
93
+ const markdownContentEl = mainEl.querySelector('.sl-markdown-content');
94
+ if (!markdownContentEl) {
95
+ return null;
96
+ }
97
+
98
+ removeHiddenElements(markdownContentEl);
99
+
100
+ const articleContent = markdownContentEl.innerHTML;
101
+
102
+ const paginationLinks: PaginationItems = {
103
+ prev: null,
104
+ next: null,
105
+ };
106
+
107
+ if (footer) {
108
+ paginationLinks.prev = parsePaginationLink(footer, 'prev');
109
+ paginationLinks.next = parsePaginationLink(footer, 'next');
110
+ }
111
+
112
+ let md = (
113
+ await unified()
114
+ .use(rehypeParse, { fragment: true }) // parse HTML
115
+ .use(rehypeRemark) // rehype (HTML) -> remark (MD AST)
116
+ .use(remarkGfm) // tables, strikethrough, autolinks, etc.
117
+ .use(remarkStringify, {
118
+ fences: true,
119
+ bullet: '-',
120
+ listItemIndent: 'one',
121
+ rule: '-',
122
+ })
123
+ .process(articleContent)
124
+ ).toString();
125
+
126
+ const title = root.querySelector('title')?.textContent;
127
+ const description = root.querySelector('meta[name="description"]')?.attributes.content;
128
+ const lastUpdated = root.querySelector('.meta time')?.attributes.datetime;
129
+
130
+ // let htmlUrl = url.toString().replace('.md', '');
131
+ // if (htmlUrl.endsWith('/index')) {
132
+ // htmlUrl = htmlUrl.replace('/index', '');
133
+ // }
134
+
135
+ md = [
136
+ '---',
137
+ `title: ${title}`,
138
+ description ? `description: ${description}` : [],
139
+ lastUpdated ? `lastUpdated: ${lastUpdated}` : [],
140
+ // `source_url:`,
141
+ // ` html: ${htmlUrl}`,
142
+ // ` md: ${url.toString()}`,
143
+ '---\n',
144
+ md,
145
+ ]
146
+ .flat()
147
+ .join('\n');
148
+
149
+ if (paginationLinks.prev) {
150
+ md += `\n\n[Previous](${paginationLinks.prev.href})`;
151
+ }
152
+
153
+ if (paginationLinks.next) {
154
+ md += `\n\n[Next](${paginationLinks.next.href})`;
155
+ }
156
+
157
+ return md;
158
+ }