@stainless-api/docs 0.1.0-beta.3 → 0.1.0-beta.31

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 (102) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/components/variables.css +1 -27
  3. package/eslint-suppressions.json +47 -0
  4. package/locals.d.ts +14 -0
  5. package/package.json +30 -27
  6. package/plugin/buildAlgoliaIndex.ts +29 -4
  7. package/plugin/cms/server.ts +97 -54
  8. package/plugin/cms/sidebar-builder.ts +6 -25
  9. package/plugin/cms/worker.ts +2 -2
  10. package/plugin/components/SnippetCode.tsx +7 -4
  11. package/plugin/components/search/SearchAlgolia.astro +0 -7
  12. package/plugin/components/search/SearchIsland.tsx +30 -17
  13. package/plugin/generateAPIReferenceLink.ts +1 -1
  14. package/plugin/globalJs/ai-dropdown-options.ts +233 -0
  15. package/plugin/globalJs/navigation.ts +0 -23
  16. package/plugin/helpers/getPageLoadEvent.ts +1 -1
  17. package/plugin/index.ts +48 -18
  18. package/plugin/languages.ts +1 -1
  19. package/plugin/loadPluginConfig.ts +100 -13
  20. package/plugin/react/Routing.tsx +30 -33
  21. package/plugin/referencePlaceholderUtils.ts +1 -1
  22. package/plugin/replaceSidebarPlaceholderMiddleware.ts +4 -0
  23. package/plugin/routes/Docs.astro +59 -85
  24. package/plugin/routes/Overview.astro +9 -15
  25. package/plugin/routes/markdown.ts +3 -3
  26. package/plugin/vendor/preview.worker.docs.js +7566 -6784
  27. package/resolveSrcFile.ts +10 -0
  28. package/shared/getSharedLogger.ts +15 -0
  29. package/shared/terminalUtils.ts +3 -0
  30. package/src/content.config.ts +9 -0
  31. package/stl-docs/components/AIDropdown.tsx +63 -0
  32. package/stl-docs/components/Head.astro +16 -0
  33. package/stl-docs/components/Header.astro +3 -2
  34. package/stl-docs/components/PageTitle.astro +64 -0
  35. package/stl-docs/components/TableOfContents.astro +34 -0
  36. package/stl-docs/components/ThemeSelect.astro +4 -2
  37. package/stl-docs/components/content-panel/ContentPanel.astro +9 -39
  38. package/stl-docs/components/headers/DefaultHeader.astro +1 -1
  39. package/stl-docs/components/headers/HeaderLinks.astro +1 -1
  40. package/stl-docs/components/headers/StackedHeader.astro +29 -24
  41. package/stl-docs/components/icons/chat-gpt.tsx +17 -0
  42. package/stl-docs/components/icons/claude.tsx +10 -0
  43. package/stl-docs/components/icons/cursor.tsx +46 -0
  44. package/stl-docs/components/icons/gemini.tsx +30 -0
  45. package/stl-docs/components/icons/markdown.tsx +10 -0
  46. package/stl-docs/components/index.ts +2 -0
  47. package/stl-docs/components/mintlify-compat/Accordion.astro +7 -38
  48. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +9 -23
  49. package/stl-docs/components/mintlify-compat/Columns.astro +40 -42
  50. package/stl-docs/components/mintlify-compat/Frame.astro +16 -18
  51. package/stl-docs/components/mintlify-compat/Step.astro +30 -32
  52. package/stl-docs/components/mintlify-compat/Steps.astro +8 -10
  53. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +10 -3
  54. package/stl-docs/components/mintlify-compat/callouts/Check.astro +7 -3
  55. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +7 -3
  56. package/stl-docs/components/mintlify-compat/callouts/Info.astro +7 -3
  57. package/stl-docs/components/mintlify-compat/callouts/Note.astro +7 -3
  58. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +7 -3
  59. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +7 -3
  60. package/stl-docs/components/mintlify-compat/card.css +33 -35
  61. package/stl-docs/components/nav-tabs/NavDropdown.astro +1 -1
  62. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -7
  63. package/stl-docs/components/nav-tabs/buildNavLinks.ts +4 -3
  64. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  65. package/stl-docs/components/pagination/Pagination.astro +173 -0
  66. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  67. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  68. package/stl-docs/components/pagination/util.ts +71 -0
  69. package/stl-docs/components/{Sidebar.astro → sidebars/BaseSidebar.astro} +2 -3
  70. package/stl-docs/components/sidebars/SDKSelectSidebar.astro +8 -0
  71. package/stl-docs/disableCalloutSyntax.ts +36 -0
  72. package/stl-docs/index.ts +76 -26
  73. package/stl-docs/loadStlDocsConfig.ts +25 -3
  74. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +64 -0
  75. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +34 -0
  76. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  77. package/stl-docs/tabsMiddleware.ts +12 -4
  78. package/styles/code.css +115 -127
  79. package/styles/fonts.css +32 -17
  80. package/styles/links.css +10 -49
  81. package/styles/overrides.css +54 -57
  82. package/styles/page.css +89 -59
  83. package/styles/sdk_select.css +6 -7
  84. package/styles/search.css +65 -67
  85. package/styles/sidebar.css +199 -128
  86. package/styles/toc.css +37 -33
  87. package/theme.css +9 -1
  88. package/tsconfig.json +2 -5
  89. package/virtual-module.d.ts +5 -1
  90. package/plugin/globalJs/ai-dropdown.ts +0 -57
  91. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -86
  92. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -64
  93. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  94. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  95. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  96. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  97. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  98. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  99. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  100. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  101. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
  102. /package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +0 -0
@@ -0,0 +1,233 @@
1
+ import { initDropdownButton } from '@stainless-api/ui-primitives/scripts';
2
+ import { getPageLoadEvent } from '../helpers/getPageLoadEvent';
3
+
4
+ export type DropdownIcon = 'markdown' | 'copy' | 'claude' | 'chatgpt' | 'gemini' | 'cursor';
5
+
6
+ interface DropdownOptionInputProps {
7
+ onClick: () => void;
8
+ icon: DropdownIcon;
9
+ primaryAction?: boolean;
10
+ clientHidden?: boolean;
11
+ external: boolean;
12
+ }
13
+
14
+ function option(label: string[] | string, props: DropdownOptionInputProps) {
15
+ const labelArr = typeof label === 'string' ? [label] : label;
16
+ return {
17
+ ...props,
18
+ label: labelArr,
19
+ primaryAction: props.primaryAction ?? false,
20
+ clientHidden: props.clientHidden ?? false,
21
+ id: labelArr.join('').toLowerCase().replace(/ /g, '-'),
22
+ };
23
+ }
24
+
25
+ type DropdownOption = ReturnType<typeof option>;
26
+
27
+ function getMarkdownUrl(type: 'relative' | 'absolute') {
28
+ const currentUrl = new URL(window.location.href);
29
+ const hasTrailingSlash = currentUrl.pathname.endsWith('/');
30
+
31
+ const markdownUrl = [
32
+ type === 'absolute' ? currentUrl.origin : '',
33
+ currentUrl.pathname,
34
+ hasTrailingSlash ? '' : '/',
35
+ 'index.md',
36
+ ].join('');
37
+
38
+ return markdownUrl;
39
+ }
40
+
41
+ function getURLEncodedPrompt() {
42
+ const mdUrl = getMarkdownUrl('absolute');
43
+ const aiPrompt = encodeURIComponent(
44
+ `Load the contents of ${mdUrl} into this chat's context so we can discuss it.`,
45
+ );
46
+ return aiPrompt;
47
+ }
48
+
49
+ function openDeepLink({ deepLinkUrl, fallbackUrl }: { deepLinkUrl: string; fallbackUrl: string }) {
50
+ if (navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrom')) {
51
+ // safari doesn't let us detect if the deep link worked
52
+ window.open(fallbackUrl, '_blank');
53
+ return;
54
+ }
55
+
56
+ const controller = new AbortController();
57
+ let frame: HTMLIFrameElement | null;
58
+
59
+ // We are using a native deep link with a fallback web url.
60
+ if (navigator.userAgent.includes('Chrom')) {
61
+ // In Chrome, load it in a hidden frame, this shows the "Do you want to open ...?" prompt, but unlike
62
+ // top level navigation this preserves our userActivation, so we can open the fallback if it fails.
63
+ frame = Object.assign(document.createElement('iframe'), { src: deepLinkUrl });
64
+ document.head.append(frame);
65
+ } else {
66
+ // In Firefox do the opposite.
67
+ location.href = deepLinkUrl;
68
+ }
69
+
70
+ // The popup (in non-Safari browsers) fires a `blur` event.
71
+ window.addEventListener(
72
+ 'blur',
73
+ () => {
74
+ controller.abort();
75
+ },
76
+ controller,
77
+ );
78
+
79
+ // If it's been 300ms with no popup, open the fallback web url.
80
+ const timeout = setTimeout(() => {
81
+ window.open(fallbackUrl, '_blank');
82
+ }, 300);
83
+
84
+ controller.signal.addEventListener('abort', () => {
85
+ clearTimeout(timeout);
86
+ frame?.remove();
87
+ });
88
+ }
89
+
90
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Browser_detection_using_the_user_agent#mobile_tablet_or_desktop
91
+ const hasMobileUserAgent = navigator.userAgent.includes('Mobi');
92
+
93
+ // 2d array of dropdown options
94
+ // each sub-array is a group, separated by a horizontal rule in the UI
95
+ const aiDropdownOptions: DropdownOption[][] = [
96
+ [
97
+ option(['Open in ', 'Claude'], {
98
+ onClick: () => {
99
+ window.open(`https://claude.ai/new?q=${getURLEncodedPrompt()}`, '_blank');
100
+ },
101
+ icon: 'claude',
102
+ primaryAction: false,
103
+ external: true,
104
+ }),
105
+ option(['Open in ', 'ChatGPT'], {
106
+ onClick: () => {
107
+ window.open(`https://chatgpt.com/?hints=search&prompt=${getURLEncodedPrompt()}`, '_blank');
108
+ },
109
+ icon: 'chatgpt',
110
+ primaryAction: false,
111
+ external: true,
112
+ }),
113
+ option(['Open in ', 'Cursor'], {
114
+ onClick: () => {
115
+ const aiPrompt = getURLEncodedPrompt();
116
+ openDeepLink({
117
+ deepLinkUrl: `cursor://anysphere.cursor-deeplink/prompt?text=${aiPrompt}`,
118
+ fallbackUrl: `https://cursor.com/link/prompt?text=${aiPrompt}`,
119
+ });
120
+ },
121
+ clientHidden: hasMobileUserAgent,
122
+ icon: 'cursor',
123
+ primaryAction: false,
124
+ external: true,
125
+ }),
126
+ ],
127
+ [
128
+ option('Copy Markdown', {
129
+ onClick: () => {
130
+ // Source: https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/
131
+ const markdownUrl = getMarkdownUrl('relative');
132
+ // ClipboardItem doesn't exist in every browser
133
+ // eslint-disable-next-line no-constant-binary-expression
134
+ if (typeof ClipboardItem && navigator.clipboard.write) {
135
+ // NOTE: Safari locks down the clipboard API to only work when triggered
136
+ // by a direct user interaction. You can't use it async in a promise.
137
+ // But! You can wrap the promise in a ClipboardItem, and give that to
138
+ // the clipboard API.
139
+ // Found this on https://developer.apple.com/forums/thread/691873
140
+ const text = new ClipboardItem({
141
+ 'text/plain': fetch(markdownUrl)
142
+ .then((response) => response.text())
143
+ .then((text) => new Blob([text], { type: 'text/plain' })),
144
+ });
145
+ navigator.clipboard.write([text]);
146
+ } else {
147
+ // NOTE: Firefox has support for ClipboardItem and navigator.clipboard.write,
148
+ // but those are behind `dom.events.asyncClipboard.clipboardItem` preference.
149
+ // Good news is that other than Safari, Firefox does not care about
150
+ // Clipboard API being used async in a Promise.
151
+ fetch(markdownUrl)
152
+ .then((response) => response.text())
153
+ .then((text) => navigator.clipboard.writeText(text));
154
+ }
155
+ },
156
+ icon: 'copy',
157
+ primaryAction: true,
158
+ external: false,
159
+ }),
160
+ option('View as Markdown', {
161
+ onClick: () => {
162
+ window.open(getMarkdownUrl('absolute'), '_blank');
163
+ },
164
+ icon: 'markdown',
165
+ primaryAction: false,
166
+ external: true,
167
+ }),
168
+ ],
169
+ ];
170
+
171
+ // TODO: Add support for more LLMs
172
+ // {
173
+ // label: ['Open in ', 'Gemini'],
174
+ // onClick: () => {
175
+ // openInLLM('https://gemini.google.com?prompt_action=prefill&prompt_text=');
176
+ // },
177
+ // icon: 'gemini',
178
+ // primaryAction: false,
179
+ // },
180
+
181
+ export function getAIDropdownOptions() {
182
+ const renderedOptions = aiDropdownOptions.map((group, index) => {
183
+ return {
184
+ options: group,
185
+ isLast: index === aiDropdownOptions.length - 1,
186
+ reactKey: index,
187
+ };
188
+ });
189
+
190
+ const allOptions = renderedOptions.flatMap((group) => group.options);
191
+ const primaryAction = allOptions.find((o) => o.primaryAction) ?? allOptions[0]!;
192
+
193
+ return {
194
+ primaryAction,
195
+ groups: renderedOptions,
196
+ };
197
+ }
198
+
199
+ export function wireAIDropdown() {
200
+ const { primaryAction, groups } = getAIDropdownOptions();
201
+ const flatOptions = groups.flatMap((group) => group.options);
202
+ function triggerOption(id: string) {
203
+ const option = flatOptions.find((option) => option.id === id);
204
+ if (!option) return;
205
+ option.onClick();
206
+ }
207
+
208
+ document.addEventListener(getPageLoadEvent(), () => {
209
+ // we hide the Cursor option on non-desktop devices
210
+ for (const option of flatOptions) {
211
+ if (option.clientHidden === true) {
212
+ const el = document.querySelector(`[data-value="${option.id}"]`);
213
+ if (el) {
214
+ el.remove();
215
+ }
216
+ }
217
+ }
218
+
219
+ const dropdowns = document.querySelectorAll('[data-dropdown-id]');
220
+
221
+ dropdowns.forEach((dropdown) => {
222
+ initDropdownButton({
223
+ dropdown: dropdown,
224
+ onSelect: (value) => {
225
+ triggerOption(value);
226
+ },
227
+ onPrimaryAction: () => {
228
+ triggerOption(primaryAction.id);
229
+ },
230
+ });
231
+ });
232
+ });
233
+ }
@@ -5,8 +5,6 @@ import { navigate } from 'astro:transitions/client';
5
5
  import { getPageLoadEvent } from '../helpers/getPageLoadEvent.ts';
6
6
 
7
7
  import { initDropdown } from '@stainless-api/docs-ui/src/components/scripts/dropdown';
8
- import { initDropdownButton } from '@stainless-api/ui-primitives/scripts';
9
- import { copyCurrentPageAsMarkdown, onSelectAIOption } from './ai-dropdown.ts';
10
8
 
11
9
  history.scrollRestoration = 'auto';
12
10
 
@@ -36,27 +34,6 @@ document.addEventListener(getPageLoadEvent(), () => {
36
34
  if (path) setTimeout(() => scrollToPath(path.slice(1)), 10);
37
35
  });
38
36
 
39
- document.addEventListener(getPageLoadEvent(), () => {
40
- console.log('Initializing AI Dropdown');
41
- initDropdownButton({
42
- dropdownId: 'ai-dropdown-button',
43
- onSelect: onSelectAIOption,
44
- onPrimaryAction: (el) => {
45
- copyCurrentPageAsMarkdown();
46
- const innerText = el.querySelector('[data-part="primary-action-text"]');
47
- if (!innerText) return;
48
-
49
- const originalInnerHtml = innerText.innerHTML;
50
- innerText.innerHTML = 'Copied!';
51
- el.classList.add('disabled');
52
- setTimeout(() => {
53
- innerText.innerHTML = originalInnerHtml;
54
- el.classList.remove('disabled');
55
- }, 1000);
56
- },
57
- });
58
- });
59
-
60
37
  document.addEventListener('click', (event) => {
61
38
  const toggle = (event.target as HTMLElement).closest(
62
39
  '[data-stldocs-property-toggle-expanded] > .stldocs-expand-toggle-content',
@@ -1,4 +1,4 @@
1
- // import { ENABLE_CLIENT_ROUTER } from 'virtual:stl-stl-starlight-virtual-module';
1
+ // import { ENABLE_CLIENT_ROUTER } from 'virtual:stl-docs-virtual-module';
2
2
 
3
3
  export function getPageLoadEvent() {
4
4
  // if (ENABLE_CLIENT_ROUTER) {
package/plugin/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import react from '@astrojs/react';
2
2
  import type { StarlightPlugin } from '@astrojs/starlight/types';
3
- import type { AstroIntegration } from 'astro';
3
+ import type { AstroIntegration, AstroIntegrationLogger } from 'astro';
4
4
  import { config } from 'dotenv';
5
5
  import getPort from 'get-port';
6
6
  import { startDevServer } from './cms/server';
@@ -23,11 +23,15 @@ import {
23
23
  import { buildVirtualModuleString } from '../shared/virtualModule';
24
24
  import path from 'path';
25
25
  import fs from 'fs';
26
+ import { getSharedLogger } from '../shared/getSharedLogger';
27
+ import { resolveSrcFile } from '../resolveSrcFile';
26
28
 
27
29
  export { generateAPILink } from './generateAPIReferenceLink';
28
30
  export type { ReferenceSidebarConfigItem };
29
31
 
30
- config();
32
+ config({
33
+ quiet: true,
34
+ });
31
35
 
32
36
  let sidebarIdCounter = 0;
33
37
 
@@ -108,6 +112,7 @@ function tmpGetCMSServerConfig(specRetrieverConfig: SpecRetrieverConfig) {
108
112
 
109
113
  async function stlStarlightAstroIntegration(
110
114
  pluginConfig: NormalizedStainlessStarlightConfig,
115
+ stlStarlightPluginLogger: AstroIntegrationLogger,
111
116
  ): Promise<AstroIntegration> {
112
117
  const virtualId = `virtual:stl-starlight-virtual-module`;
113
118
  // The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
@@ -117,11 +122,12 @@ async function stlStarlightAstroIntegration(
117
122
 
118
123
  const { apiKey, version, devPaths } = tmpGetCMSServerConfig(pluginConfig.specRetrieverConfig);
119
124
 
120
- const cmsServer = await startDevServer({
125
+ const cmsServer = startDevServer({
121
126
  port: CMS_PORT,
122
- apiKey,
127
+ apiKey: apiKey.value,
123
128
  version,
124
129
  devPaths,
130
+ logger: stlStarlightPluginLogger,
125
131
  getGeneratedSidebarConfig: (id: number) => {
126
132
  const config = sidebarConfigs.get(id);
127
133
  if (!config) {
@@ -134,34 +140,40 @@ async function stlStarlightAstroIntegration(
134
140
  return {
135
141
  name: 'stl-starlight-astro',
136
142
  hooks: {
137
- 'astro:config:setup': async ({ injectRoute, updateConfig, logger, command, config: astroConfig }) => {
143
+ 'astro:config:setup': async ({
144
+ injectRoute,
145
+ updateConfig,
146
+ logger: localLogger,
147
+ command,
148
+ config: astroConfig,
149
+ }) => {
150
+ const logger = getSharedLogger({ fallback: localLogger });
138
151
  const projectDir = astroConfig.root.pathname;
139
152
 
140
153
  const middlewareFile = path.join(projectDir, 'middleware.stainless.ts');
141
154
 
142
155
  let vmMiddlewareExport = 'export const MIDDLEWARE = {};';
143
156
  if (fs.existsSync(middlewareFile)) {
144
- logger.info(`Loading middleware from ${middlewareFile}`);
157
+ logger.debug(`Loading middleware from ${middlewareFile}`);
145
158
  vmMiddlewareExport = `export { default as MIDDLEWARE } from '${middlewareFile}';`;
146
159
  }
147
160
 
148
161
  injectRoute({
149
- pattern: `${pluginConfig.basePath}/[...slug].md`,
150
- entrypoint: '@stainless-api/docs/MarkdownRoute',
162
+ pattern: `${pluginConfig.basePath}/[...slug]/index.md`,
163
+ entrypoint: resolveSrcFile('/plugin/routes/markdown.ts'),
151
164
  prerender: command === 'build',
152
165
  });
153
166
 
154
- const astroFile = command === 'build' ? 'DocsStaticRoute' : 'DocsRoute';
167
+ const astroFile = command === 'build' ? 'DocsStatic' : 'Docs';
155
168
  injectRoute({
156
169
  pattern: `${pluginConfig.basePath}/[...slug]`,
157
- // in prod I think this points to @stainless-starlight/components/docs.astro
158
- entrypoint: `@stainless-api/docs/${astroFile}`,
170
+ entrypoint: resolveSrcFile(`/plugin/routes/${astroFile}.astro`),
159
171
  prerender: command === 'build',
160
172
  });
161
173
 
162
174
  injectRoute({
163
175
  pattern: pluginConfig.basePath,
164
- entrypoint: '@stainless-api/docs/OverviewRoute',
176
+ entrypoint: resolveSrcFile('/plugin/routes/Overview.astro'),
165
177
  prerender: command === 'build',
166
178
  });
167
179
 
@@ -170,7 +182,6 @@ async function stlStarlightAstroIntegration(
170
182
  ssr: {
171
183
  noExternal: ['@stainless-api/ui-primitives'],
172
184
  },
173
- optimizeDeps: { include: ['@stainless-api/ui-primitives'] },
174
185
  plugins: [
175
186
  {
176
187
  name: 'stl-starlight-vite',
@@ -213,6 +224,7 @@ async function stlStarlightAstroIntegration(
213
224
  EXPERIMENTAL_COLLAPSIBLE_SNIPPETS: pluginConfig.experimentalCollapsibleSnippets,
214
225
  PROPERTY_SETTINGS: pluginConfig.propertySettings,
215
226
  SEARCH: pluginConfig.search,
227
+ ENABLE_CONTEXT_MENU: pluginConfig.contextMenu,
216
228
  }),
217
229
  vmMiddlewareExport,
218
230
  ].join('\n');
@@ -241,15 +253,20 @@ export function stainlessStarlight(someUserConfig: SomeStainlessStarlightUserCon
241
253
  command,
242
254
  config: starlightConfig,
243
255
  astroConfig,
244
- logger,
256
+ logger: localLogger,
245
257
  }) => {
246
258
  if (command !== 'build' && command !== 'dev') {
247
259
  return;
248
260
  }
249
261
 
262
+ const logger = getSharedLogger({ fallback: localLogger });
263
+
250
264
  const configParseResult = parseStarlightPluginConfig(someUserConfig, command);
251
265
  if (configParseResult.result === 'error') {
252
- logger.error(configParseResult.message);
266
+ const errorLines = configParseResult.message.split('\n');
267
+ for (const line of errorLines) {
268
+ logger.error(line);
269
+ }
253
270
  process.exit(1);
254
271
  }
255
272
  const config = configParseResult.config;
@@ -260,17 +277,30 @@ export function stainlessStarlight(someUserConfig: SomeStainlessStarlightUserCon
260
277
  addIntegration(react());
261
278
  }
262
279
 
280
+ if ('apiKey' in config.specRetrieverConfig) {
281
+ if (!config.specRetrieverConfig.apiKey) {
282
+ logger.info(`Stainless credentials not loaded`);
283
+ } else if (config.specRetrieverConfig.apiKey.source === 'explicit-config') {
284
+ logger.info(`Stainless credentials loaded from user config`);
285
+ } else if (config.specRetrieverConfig.apiKey.source === 'environment-variable') {
286
+ logger.info('Stainless credentials loaded from `STAINLESS_API_KEY` environment variable');
287
+ } else if (config.specRetrieverConfig.apiKey.source === 'cli') {
288
+ logger.info('Stainless credentials loaded from `stl` CLI');
289
+ }
290
+ }
291
+
263
292
  if (
264
293
  command === 'build' &&
265
294
  config.specRetrieverConfig.kind === 'local_spec_server_with_remote_files'
266
295
  ) {
267
296
  await buildAlgoliaIndex({
268
297
  version: config.specRetrieverConfig.version,
269
- apiKey: config.specRetrieverConfig.apiKey,
298
+ apiKey: config.specRetrieverConfig.apiKey.value,
299
+ logger,
270
300
  });
271
301
  }
272
302
 
273
- addIntegration(await stlStarlightAstroIntegration(config));
303
+ addIntegration(await stlStarlightAstroIntegration(config, logger));
274
304
 
275
305
  if (starlightConfig.sidebar) {
276
306
  // for pagination (https://starlight.astro.build/reference/configuration/#pagination) to work correctly
@@ -295,7 +325,7 @@ export function stainlessStarlight(someUserConfig: SomeStainlessStarlightUserCon
295
325
  });
296
326
 
297
327
  addRouteMiddleware({
298
- entrypoint: '@stainless-api/docs/replaceSidebarPlaceholderMiddleware',
328
+ entrypoint: resolveSrcFile('/plugin/replaceSidebarPlaceholderMiddleware.ts'),
299
329
  order: 'post',
300
330
  });
301
331
  },
@@ -44,7 +44,7 @@ export function applyLanguageToLinks(basePath?: string, defaultLanguage?: string
44
44
  `[data-stldocs-overview],[data-stldocs-method],a.nav-link[href^='${basePath}']`,
45
45
  );
46
46
 
47
- for (var link of links) {
47
+ for (const link of links) {
48
48
  const href = link.getAttribute('href');
49
49
  const prefix = generatePrefix(basePath, language);
50
50
  if (href?.startsWith(basePath) && !href?.startsWith(prefix)) {
@@ -1,9 +1,12 @@
1
1
  import path from 'path';
2
+ import { homedir } from 'os';
3
+ import { existsSync, readFileSync } from 'fs';
2
4
 
3
5
  import type { CreateShikiHighlighterOptions } from '@astrojs/markdown-remark';
4
6
  import type { DocsLanguage } from '@stainless-api/docs-ui/src/routing';
5
7
  import type { PropertySettingsType } from '@stainless-api/docs-ui/src/contexts';
6
8
  import type { InputFilePaths } from '../plugin/cms/server';
9
+ import { bold } from '../shared/terminalUtils';
7
10
 
8
11
  export type AstroCommand = 'dev' | 'build' | 'preview' | 'sync';
9
12
 
@@ -18,7 +21,7 @@ export type VersionUserConfig = {
18
21
  type BreadcrumbUserConfig = {
19
22
  /**
20
23
  * Include the current page in the breadcrumb list.
21
- * Defaults to `false`.
24
+ * Default: `false`
22
25
  */
23
26
  includeCurrentPage?: boolean;
24
27
  };
@@ -26,7 +29,12 @@ type BreadcrumbUserConfig = {
26
29
  export type StainlessStarlightUserConfig = {
27
30
  /**
28
31
  * Optional api key for Stainless API.
29
- * If not provided, it will look for the STAINLESS_API_KEY environment variable.
32
+ * If not provided, we will handle Stainless auth via the `stl` CLI or look for the STAINLESS_API_KEY environment variable.
33
+ * Precedence:
34
+ * 1. Explicity `apiKey` option provided
35
+ * 2. `STAINLESS_API_KEY` environment variable
36
+ * 3. Login status from the `stl` CLI
37
+ * 4. Error (no auth found)
30
38
  */
31
39
  apiKey?: string;
32
40
 
@@ -42,7 +50,7 @@ export type StainlessStarlightUserConfig = {
42
50
 
43
51
  /**
44
52
  * Optional mount point for API reference docs.
45
- * Defaults to `/api`.
53
+ * Default: `/api`
46
54
  * Example: `/my-api` → docs available at `/my-api/…`.
47
55
  */
48
56
  basePath?: string;
@@ -55,7 +63,7 @@ export type StainlessStarlightUserConfig = {
55
63
 
56
64
  /**
57
65
  * Optional language to treat as the default when the user hasn't selected one.
58
- * Defaults to `"http"` when none is provided.
66
+ * Default: `"http"`
59
67
  * Example: `"python"`
60
68
  */
61
69
  defaultLanguage?: DocsLanguage;
@@ -90,7 +98,7 @@ export type StainlessStarlightUserConfig = {
90
98
  contentPanel?: {
91
99
  /**
92
100
  * Optional layout for the content panel.
93
- * Defaults to `"double-pane"`.
101
+ * Default: `"double-pane"`
94
102
  */
95
103
  layout?: ContentLayout;
96
104
  };
@@ -107,6 +115,8 @@ export type StainlessStarlightUserConfig = {
107
115
  /**
108
116
  * When set to `true`, the enableAISearch` setting turns on support for
109
117
  * LLM-based conversations with the API documentation
118
+ *
119
+ * Default: `false`
110
120
  */
111
121
  enableAISearch?: boolean;
112
122
  };
@@ -114,9 +124,17 @@ export type StainlessStarlightUserConfig = {
114
124
  /**
115
125
  * Enable experimental collapsible code snippets. Snippets will be collapsed by default for
116
126
  * single-pane and mobile layouts.
117
- * Defaults to `false`.
127
+ *
128
+ * Default: `false`
118
129
  */
119
130
  experimentalCollapsibleSnippets?: boolean;
131
+
132
+ /**
133
+ * Whether to show the context menu with options like "Copy as Markdown" and "Open in ChatGPT".
134
+ *
135
+ * Default: `true`
136
+ */
137
+ contextMenu?: boolean;
120
138
  };
121
139
 
122
140
  export type ExternalSpecServerUserConfig = Omit<StainlessStarlightUserConfig, 'stainlessProject'> & {
@@ -133,15 +151,29 @@ function getLocalFilePaths(command: AstroCommand): InputFilePaths | null {
133
151
  if (command !== 'dev') {
134
152
  return null;
135
153
  }
136
- if (!process.env.OPENAPI_PATH || !process.env.STAINLESS_SPEC_PATH) {
154
+
155
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
156
+ const oasPath = process.env.OPENAPI_PATH;
157
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
158
+ const configPath = process.env.STAINLESS_CONFIG_PATH;
159
+
160
+ if (!oasPath || !configPath) {
137
161
  return null;
138
162
  }
163
+
139
164
  return {
140
- oasPath: resolvePath(process.env.OPENAPI_PATH),
141
- configPath: resolvePath(process.env.STAINLESS_SPEC_PATH),
165
+ oasPath: resolvePath(oasPath),
166
+ configPath: resolvePath(configPath),
142
167
  };
143
168
  }
144
169
 
170
+ export type ApiKeySource = 'explicit-config' | 'environment-variable' | 'cli';
171
+
172
+ export type LoadedApiKey = {
173
+ value: string;
174
+ source: ApiKeySource;
175
+ };
176
+
145
177
  export type SpecRetrieverConfig =
146
178
  | {
147
179
  kind: 'external_spec_server';
@@ -152,16 +184,63 @@ export type SpecRetrieverConfig =
152
184
  kind: 'local_spec_server_with_files';
153
185
  stainlessProject: string;
154
186
  devPaths: InputFilePaths;
155
- apiKey: string | null;
187
+ apiKey: LoadedApiKey | null;
156
188
  version: VersionUserConfig;
157
189
  }
158
190
  | {
159
191
  kind: 'local_spec_server_with_remote_files';
160
192
  stainlessProject: string;
161
- apiKey: string;
193
+ apiKey: LoadedApiKey;
162
194
  version: VersionUserConfig;
163
195
  };
164
196
 
197
+ function parseAuthJson(authJsonStr: string) {
198
+ let json: unknown;
199
+ try {
200
+ json = JSON.parse(authJsonStr);
201
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
202
+ } catch (_error) {
203
+ return null;
204
+ }
205
+
206
+ if (typeof json !== 'object' || json === null) {
207
+ return null;
208
+ }
209
+ if (!('access_token' in json)) {
210
+ return null;
211
+ }
212
+ const accessToken = json['access_token'];
213
+ if (typeof accessToken !== 'string') {
214
+ return null;
215
+ }
216
+ return accessToken;
217
+ }
218
+
219
+ function loadApiKey(configValue: string | undefined): LoadedApiKey | null {
220
+ if (typeof configValue === 'string') {
221
+ return { value: configValue, source: 'explicit-config' };
222
+ }
223
+ if (process.env.STAINLESS_API_KEY) {
224
+ return { value: process.env.STAINLESS_API_KEY, source: 'environment-variable' };
225
+ }
226
+
227
+ const homeDirPath = homedir();
228
+
229
+ const authJsonPath = path.join(homeDirPath, '.config', 'stainless', 'auth.json');
230
+
231
+ if (!existsSync(authJsonPath)) {
232
+ return null;
233
+ }
234
+
235
+ const authJsonStr = readFileSync(authJsonPath, 'utf-8');
236
+ const accessToken = parseAuthJson(authJsonStr);
237
+ if (!accessToken) {
238
+ return null;
239
+ }
240
+
241
+ return { value: accessToken, source: 'cli' };
242
+ }
243
+
165
244
  function normalizeConfig(partial: SomeStainlessStarlightUserConfig, command: AstroCommand) {
166
245
  const configWithDefaults = {
167
246
  basePath: partial.basePath ?? '/api',
@@ -190,6 +269,7 @@ function normalizeConfig(partial: SomeStainlessStarlightUserConfig, command: Ast
190
269
  search: {
191
270
  enableAISearch: partial.search?.enableAISearch ?? false,
192
271
  },
272
+ contextMenu: partial.contextMenu ?? true,
193
273
  };
194
274
 
195
275
  function getSpecRetrieverConfig(): SpecRetrieverConfig {
@@ -205,7 +285,7 @@ function normalizeConfig(partial: SomeStainlessStarlightUserConfig, command: Ast
205
285
  throw new Error('You must provide a stainlessProject when using Stainless Starlight');
206
286
  }
207
287
 
208
- const apiKey = partial.apiKey ?? process.env.STAINLESS_API_KEY ?? null;
288
+ const apiKey = loadApiKey(partial.apiKey);
209
289
 
210
290
  const version = {
211
291
  stainlessProject: partial.stainlessProject,
@@ -226,7 +306,14 @@ function normalizeConfig(partial: SomeStainlessStarlightUserConfig, command: Ast
226
306
 
227
307
  if (!apiKey) {
228
308
  throw new Error(
229
- 'Please provide a Stainless API key via the STAINLESS_API_KEY environment variable or the apiKey option in the Stainless Starlight config.',
309
+ [
310
+ bold(
311
+ 'No Stainless credentials found. Please choose one of the following options to authenticate with Stainless:',
312
+ ),
313
+ '- Run `stl auth login` to authenticate via the Stainless CLI',
314
+ '- Provide a Stainless API key via the `STAINLESS_API_KEY` environment variable (eg. in a .env file)',
315
+ '- Set the `apiKey` option in the Stainless Docs config',
316
+ ].join('\n'),
230
317
  );
231
318
  }
232
319