@stainless-api/docs 0.1.0-beta.8 → 0.1.0-beta.80

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