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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/CHANGELOG.md +1102 -0
  2. package/ambient.d.ts +6 -0
  3. package/eslint-suppressions.json +90 -0
  4. package/{eslint.config.js → eslint.config.ts} +0 -2
  5. package/locals.d.ts +17 -0
  6. package/package.json +62 -44
  7. package/playground-virtual-modules.d.ts +96 -0
  8. package/plugin/assets/languages/cli.svg +14 -0
  9. package/plugin/assets/languages/csharp.svg +1 -0
  10. package/plugin/assets/languages/php.svg +4 -0
  11. package/plugin/buildAlgoliaIndex.ts +40 -39
  12. package/plugin/components/MethodDescription.tsx +54 -0
  13. package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
  14. package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
  15. package/plugin/components/RequestBuilder/index.tsx +40 -0
  16. package/plugin/components/RequestBuilder/props.ts +9 -0
  17. package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
  18. package/plugin/components/RequestBuilder/styles.css +67 -0
  19. package/plugin/components/SDKSelect.astro +18 -111
  20. package/plugin/components/SnippetCode.tsx +112 -70
  21. package/plugin/components/StainlessIslands.tsx +126 -0
  22. package/plugin/components/search/SearchAlgolia.astro +46 -29
  23. package/plugin/components/search/SearchIsland.tsx +61 -37
  24. package/plugin/generateAPIReferenceLink.ts +0 -40
  25. package/plugin/globalJs/ai-dropdown-options.ts +248 -0
  26. package/plugin/globalJs/code-snippets.ts +45 -16
  27. package/plugin/globalJs/copy.ts +115 -27
  28. package/plugin/globalJs/create-playground.shim.ts +3 -0
  29. package/plugin/globalJs/method-descriptions.ts +33 -0
  30. package/plugin/globalJs/navigation.ts +24 -44
  31. package/plugin/globalJs/playground-data.shim.ts +1 -0
  32. package/plugin/globalJs/playground-data.ts +14 -0
  33. package/plugin/globalJs/summary-selection-tweak.ts +29 -0
  34. package/plugin/helpers/generateDocsRoutes.ts +59 -0
  35. package/plugin/helpers/multiSpec.ts +8 -0
  36. package/plugin/index.ts +317 -141
  37. package/plugin/languages.ts +8 -2
  38. package/plugin/loadPluginConfig.ts +284 -109
  39. package/plugin/markdown/highlighter.ts +100 -0
  40. package/plugin/markdown/index.ts +39 -0
  41. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
  42. package/plugin/react/Routing.tsx +98 -263
  43. package/plugin/referencePlaceholderUtils.ts +17 -14
  44. package/plugin/replaceSidebarPlaceholderMiddleware.ts +39 -35
  45. package/plugin/routes/Docs.astro +72 -111
  46. package/plugin/routes/DocsStatic.astro +6 -5
  47. package/plugin/routes/Overview.astro +46 -22
  48. package/plugin/routes/llms.ts +186 -0
  49. package/plugin/routes/markdown.ts +13 -12
  50. package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +84 -69
  51. package/plugin/specs/FileCache.ts +99 -0
  52. package/plugin/specs/fetchSpecSSR.ts +27 -0
  53. package/plugin/specs/generateSpec.ts +112 -0
  54. package/plugin/specs/index.ts +132 -0
  55. package/plugin/specs/inputResolver.ts +148 -0
  56. package/plugin/{cms → specs}/worker.ts +82 -5
  57. package/plugin/vendor/preview.worker.docs.js +27121 -16890
  58. package/plugin/vendor/templates/cli.md +1 -0
  59. package/plugin/vendor/templates/go.md +4 -2
  60. package/plugin/vendor/templates/java.md +5 -1
  61. package/plugin/vendor/templates/kotlin.md +5 -1
  62. package/plugin/vendor/templates/node.md +4 -2
  63. package/plugin/vendor/templates/python.md +4 -2
  64. package/plugin/vendor/templates/ruby.md +4 -2
  65. package/plugin/vendor/templates/terraform.md +1 -1
  66. package/plugin/vendor/templates/typescript.md +3 -1
  67. package/resolveSrcFile.ts +10 -0
  68. package/scripts/vendor_deps.ts +5 -5
  69. package/shared/conditionalIntegration.ts +28 -0
  70. package/shared/getProsePages.ts +41 -0
  71. package/shared/getSharedLogger.ts +15 -0
  72. package/shared/terminalUtils.ts +3 -0
  73. package/shared/virtualModule.ts +46 -1
  74. package/src/content.config.ts +9 -0
  75. package/stl-docs/aiChatExamples.ts +95 -0
  76. package/stl-docs/chat/docs-chat-handler.ts +18 -0
  77. package/stl-docs/chat/hook.ts +215 -0
  78. package/stl-docs/chat/schemas.ts +70 -0
  79. package/stl-docs/chat/stainless-handler/index.ts +126 -0
  80. package/stl-docs/chat/stream-util.ts +16 -0
  81. package/stl-docs/chat/ui/AiChat.module.css +591 -0
  82. package/stl-docs/chat/ui/AiChat.tsx +188 -0
  83. package/stl-docs/chat/ui/Trigger.tsx +154 -0
  84. package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
  85. package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
  86. package/stl-docs/chat/ui/components/ChatLog.tsx +96 -0
  87. package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
  88. package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
  89. package/stl-docs/chat/ui/components/MessageFeedback.tsx +109 -0
  90. package/stl-docs/chat/ui/components/Table.tsx +15 -0
  91. package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
  92. package/stl-docs/chat/ui/components/hljs-github.css +81 -0
  93. package/stl-docs/chat/ui/scroll-manager.ts +86 -0
  94. package/stl-docs/chat/ui/types.ts +45 -0
  95. package/stl-docs/components/AIDropdown.tsx +63 -0
  96. package/stl-docs/components/AiChatIsland.tsx +16 -0
  97. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
  98. package/stl-docs/components/ContentPanel.astro +9 -0
  99. package/stl-docs/components/Footer.astro +89 -0
  100. package/stl-docs/components/Head.astro +20 -0
  101. package/stl-docs/components/Header.astro +3 -9
  102. package/stl-docs/components/PageFrame.astro +37 -0
  103. package/stl-docs/components/PageSidebar.astro +11 -0
  104. package/stl-docs/components/PageTitle.astro +82 -0
  105. package/stl-docs/components/StainlessLogo.svg +4 -0
  106. package/stl-docs/components/ThemeProvider.astro +36 -0
  107. package/stl-docs/components/ThemeSelect.astro +84 -146
  108. package/stl-docs/components/TwoColumnContent.astro +2 -0
  109. package/stl-docs/components/headers/DefaultHeader.astro +6 -8
  110. package/stl-docs/components/headers/StackedHeader.astro +10 -53
  111. package/stl-docs/components/icons/chat-gpt.tsx +2 -2
  112. package/stl-docs/components/icons/cursor.tsx +10 -0
  113. package/stl-docs/components/icons/gemini.tsx +19 -0
  114. package/stl-docs/components/icons/markdown.tsx +1 -1
  115. package/stl-docs/components/index.ts +1 -0
  116. package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
  117. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
  118. package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
  119. package/stl-docs/components/mintlify-compat/Frame.astro +6 -6
  120. package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
  121. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
  122. package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
  123. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
  124. package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
  125. package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
  126. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
  127. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
  128. package/stl-docs/components/mintlify-compat/card.css +4 -4
  129. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  130. package/stl-docs/components/nav-tabs/NavDropdown.astro +38 -77
  131. package/stl-docs/components/nav-tabs/NavTabs.astro +81 -81
  132. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +1 -2
  133. package/stl-docs/components/nav-tabs/buildNavLinks.ts +5 -2
  134. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  135. package/stl-docs/components/pagination/Pagination.astro +177 -0
  136. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  137. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  138. package/stl-docs/components/pagination/util.ts +71 -0
  139. package/stl-docs/components/scripts.ts +1 -0
  140. package/stl-docs/components/sidebars/BaseSidebar.astro +80 -2
  141. package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
  142. package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
  143. package/stl-docs/disableCalloutSyntax.ts +36 -0
  144. package/stl-docs/fonts.ts +186 -0
  145. package/stl-docs/index.ts +176 -58
  146. package/stl-docs/loadStlDocsConfig.ts +73 -8
  147. package/stl-docs/proseDocSync.test.ts +74 -0
  148. package/stl-docs/proseDocSync.ts +344 -0
  149. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
  150. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
  151. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  152. package/stl-docs/proseSearchIndexing.ts +218 -0
  153. package/stl-docs/tabsMiddleware.ts +14 -5
  154. package/styles/code.css +53 -49
  155. package/styles/links.css +2 -37
  156. package/styles/method-descriptions.css +36 -0
  157. package/styles/overrides.css +28 -46
  158. package/styles/page.css +228 -38
  159. package/styles/sdk_select.css +9 -6
  160. package/styles/search.css +11 -21
  161. package/styles/sidebar.css +28 -215
  162. package/styles/{variables.css → sl-variables.css} +4 -8
  163. package/styles/stldocs-variables.css +6 -0
  164. package/styles/toc.css +19 -8
  165. package/theme.css +11 -9
  166. package/tsconfig.json +1 -4
  167. package/virtual-module.d.ts +66 -8
  168. package/components/variables.css +0 -112
  169. package/plugin/cms/client.ts +0 -62
  170. package/plugin/cms/server.ts +0 -268
  171. package/plugin/globalJs/ai-dropdown.ts +0 -57
  172. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
  173. package/stl-docs/components/ClientRouterHead.astro +0 -41
  174. package/stl-docs/components/content-panel/ContentPanel.astro +0 -69
  175. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
  176. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
  177. package/stl-docs/components/mintlify-compat/Step.astro +0 -56
  178. package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
  179. package/styles/fonts.css +0 -68
  180. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  181. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  182. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  183. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  184. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  185. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  186. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  187. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  188. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
@@ -1,6 +1,20 @@
1
- import type * as SDKJSON from '~/lib/json-spec-v2/types';
2
- import { generateRoute, walkTree, type DocsLanguage } from '@stainless-api/docs-ui/src/routing';
1
+ import type * as SDKJSON from '@stainless/sdk-json';
2
+ import { generateRoute, walkTree, type DocsLanguage } from '@stainless-api/docs-ui/routing';
3
3
  import type { StarlightRouteData } from '@astrojs/starlight/route-data';
4
+ import { isResourceEntirelyUnsupported } from '@stainless-api/docs-ui/languages/terraform';
5
+
6
+ function makeMethodOrResourceKey(entry: SDKJSON.Method | SDKJSON.Resource): string {
7
+ if (entry.kind === 'http_method') {
8
+ if (entry.endpoint && entry.endpoint !== '') {
9
+ return entry.endpoint;
10
+ }
11
+ return entry.stainlessPath;
12
+ }
13
+ if (entry.kind === 'resource') {
14
+ return entry.stainlessPath;
15
+ }
16
+ throw new Error(`Unknown entry kind ${JSON.stringify(entry)}`);
17
+ }
4
18
 
5
19
  function isResourceNonEmpty(resource: SDKJSON.Resource) {
6
20
  return (
@@ -119,23 +133,12 @@ export type GeneratedSidebarConfig = {
119
133
  ) => ReferenceSidebarConfigItem[] | void;
120
134
  };
121
135
 
122
- function countKeys(obj?: Record<string, any>) {
123
- let o = obj ?? {};
124
- return Object.keys(o).length;
125
- }
126
-
127
- function getMethodDeclForLanguage(entry: SDKJSON.Method, spec: SDKJSON.Spec, language: DocsLanguage) {
128
- const decls = spec.decls[language] ?? {};
129
- const decl = decls[entry.stainlessPath];
130
- if (decl !== undefined) {
131
- if ('ident' in decl) {
132
- return decl;
133
- }
134
- }
135
- return null;
136
+ function countKeys(obj?: Record<string, unknown>) {
137
+ return Object.keys(obj ?? {}).length;
136
138
  }
137
139
 
138
- type MethodDecl = Exclude<ReturnType<typeof getMethodDeclForLanguage>, null>;
140
+ type HasIdent<T> = T extends { ident: unknown } ? T : never;
141
+ type MethodDecl = HasIdent<SDKJSON.LanguageDeclNodes[SDKJSON.SpecLanguage]>;
139
142
 
140
143
  function makeAPIOverviewPage(): UserSidebarAPIOverviewPage {
141
144
  return {
@@ -166,22 +169,26 @@ function pullOutSharedModelsResource(resources: SDKJSON.Resource[]): {
166
169
  }
167
170
 
168
171
  export class SidebarConfigItemsBuilder {
169
- private getMethodDeclForLanguage(entry: SDKJSON.Method) {
172
+ private getMethodDeclForLanguage(entry: SDKJSON.Method): MethodDecl | null {
170
173
  const decls = this.spec.decls[this.language] ?? {};
171
174
  const decl = decls[entry.stainlessPath];
172
175
  if (decl !== undefined) {
173
- if ('ident' in decl) {
174
- return decl;
176
+ if ('ident' in decl && decl.ident !== undefined) {
177
+ return decl as MethodDecl;
175
178
  }
176
179
  }
177
180
  return null;
178
181
  }
179
182
 
183
+ private isWebhookResource(resource: SDKJSON.Resource): boolean {
184
+ return resource.stainlessPath === '(resource) webhooks';
185
+ }
186
+
180
187
  private toResourceOverviewPage(entry: SDKJSON.Resource): UserSidebarResourceOverviewPage {
181
188
  return {
182
189
  kind: 'resource_overview_page',
183
- label: 'Overview',
184
- key: entry.configRef,
190
+ label: this.isWebhookResource(entry) ? 'Events' : 'Overview',
191
+ key: makeMethodOrResourceKey(entry),
185
192
  badge: undefined,
186
193
  metadata: {
187
194
  subResourceCount: countKeys(entry.subresources),
@@ -195,7 +202,7 @@ export class SidebarConfigItemsBuilder {
195
202
  return {
196
203
  kind: 'method_page',
197
204
  label: entry.title,
198
- key: entry.endpoint,
205
+ key: makeMethodOrResourceKey(entry),
199
206
  badge: undefined,
200
207
  metadata: {
201
208
  deprecated: Boolean(entry.deprecated),
@@ -208,16 +215,10 @@ export class SidebarConfigItemsBuilder {
208
215
  };
209
216
  }
210
217
 
211
- private sortByLabel<T extends UserSidebarConfigItem>(items: T[]) {
212
- // sorts in place
213
- items.sort((a, b) => {
214
- return a.label.localeCompare(b.label);
215
- });
216
- }
217
-
218
218
  private generateResourceGroup(resource: SDKJSON.Resource, collapsed: boolean): ReferenceSidebarGroup {
219
219
  const entries: ReferenceSidebarConfigItem[] = [];
220
- if (!this.options?.excludeResourceOverviewPages) {
220
+ // even if we aren't generating resource overview pages, we want to generate them for webhooks
221
+ if (!this.options?.excludeResourceOverviewPages || this.isWebhookResource(resource)) {
221
222
  entries.push(this.toResourceOverviewPage(resource));
222
223
  }
223
224
  const methods = Object.values(resource.methods ?? {});
@@ -228,7 +229,6 @@ export class SidebarConfigItemsBuilder {
228
229
  methodPages.push(this.toMethodPage(m, langDecl));
229
230
  }
230
231
  }
231
- this.sortByLabel(methodPages);
232
232
  entries.push(...methodPages);
233
233
 
234
234
  const subresources = Object.values(resource.subresources ?? {});
@@ -238,26 +238,55 @@ export class SidebarConfigItemsBuilder {
238
238
  subresourceGroups.push(this.generateResourceGroup(sub, true));
239
239
  }
240
240
  }
241
- this.sortByLabel(subresourceGroups);
242
241
  entries.push(...subresourceGroups);
243
242
 
244
243
  return {
245
244
  kind: 'group',
246
245
  badge: undefined,
247
246
  label: resource.title,
248
- resourceGroupKey: resource.configRef,
247
+ resourceGroupKey: makeMethodOrResourceKey(resource),
249
248
  entries,
250
249
  collapsed,
251
250
  };
252
251
  }
253
252
 
253
+ private generateTerraformItems(resources: SDKJSON.Resource[]) {
254
+ const entries: ReferenceSidebarConfigItem[] = [];
255
+
256
+ for (const resource of resources) {
257
+ if (isResourceEntirelyUnsupported(resource, this.spec.decls['terraform'])) {
258
+ continue;
259
+ }
260
+ entries.push({
261
+ kind: 'resource_overview_page',
262
+ label: resource.title,
263
+ key: makeMethodOrResourceKey(resource),
264
+ badge: undefined,
265
+ metadata: {
266
+ subResourceCount: countKeys(resource.subresources),
267
+ methodCount: countKeys(resource.methods),
268
+ modelCount: countKeys(resource.models),
269
+ },
270
+ });
271
+ }
272
+ return entries;
273
+ }
274
+
254
275
  public generateItems(): ReferenceSidebarConfigItem[] {
255
276
  const resourceMap = this.spec.resources;
256
- let { resources, sharedModelsResource } = pullOutSharedModelsResource(Object.values(resourceMap ?? {}));
277
+ const { resources, sharedModelsResource } = pullOutSharedModelsResource(Object.values(resourceMap ?? {}));
257
278
 
258
- const entries: ReferenceSidebarConfigItem[] = resources.filter(isResourceNonEmpty).map((r) => {
259
- return this.generateResourceGroup(r, false);
260
- });
279
+ let entries: ReferenceSidebarConfigItem[];
280
+
281
+ if (this.language === 'terraform') {
282
+ // Handle Terraform specifically
283
+ // In TF, we only render the top level resource, not the subresources.
284
+ entries = this.generateTerraformItems(resources);
285
+ } else {
286
+ entries = resources.filter(isResourceNonEmpty).map((r) => {
287
+ return this.generateResourceGroup(r, false);
288
+ });
289
+ }
261
290
 
262
291
  const includeSharedModels = this.options?.includeSharedModels ?? false;
263
292
  if (includeSharedModels && sharedModelsResource) {
@@ -276,35 +305,24 @@ export class SidebarConfigItemsBuilder {
276
305
  ) {}
277
306
  }
278
307
 
279
- export function walkSidebarConfigItems(
280
- sidebar: ReferenceSidebarConfigItem[],
281
- fn: (item: ReferenceSidebarConfigItem) => void,
282
- ) {
283
- for (const item of sidebar) {
284
- fn(item);
285
- if (item.kind === 'group') {
286
- walkSidebarConfigItems(item.entries, fn);
287
- }
288
- }
289
- }
290
-
291
308
  type SidebarEntry = StarlightRouteData['sidebar'][number];
292
309
 
293
310
  // This allows us to be a bit more forgiving to the user.
294
311
  // As long as they don't modify the key, we can still find the item.
312
+
295
313
  function getResourceOrMethod(spec: SDKJSON.Spec, endpointOrConfigRef: string) {
296
314
  for (const entry of walkTree(spec, false)) {
297
- if (entry.data.kind === 'resource' && entry.data.configRef === endpointOrConfigRef) {
298
- return entry;
315
+ if (entry.data.kind === 'model') {
316
+ continue;
299
317
  }
300
- if (entry.data.kind === 'http_method' && entry.data.endpoint === endpointOrConfigRef) {
318
+ if (makeMethodOrResourceKey(entry.data) === endpointOrConfigRef) {
301
319
  return entry;
302
320
  }
303
321
  }
304
322
  return null;
305
323
  }
306
324
 
307
- export function forceGenerateRoute({
325
+ function forceGenerateRoute({
308
326
  basePath,
309
327
  stainlessPath,
310
328
  language,
@@ -320,24 +338,17 @@ export function forceGenerateRoute({
320
338
  return route;
321
339
  }
322
340
 
323
- export type BuildSidebarParams = {
341
+ type ToStarlightSidebarParams = {
324
342
  basePath: string;
325
- currentSlug: string;
326
- };
327
-
328
- type ToStarlightSidebarParams = BuildSidebarParams & {
329
343
  spec: SDKJSON.Spec;
330
344
  entries: ReferenceSidebarConfigItem[];
331
- currentStainlessPath: string;
332
345
  currentLanguage: DocsLanguage;
333
346
  };
334
347
 
335
348
  export function toStarlightSidebar({
336
349
  basePath,
337
- currentSlug,
338
350
  spec,
339
351
  entries,
340
- currentStainlessPath,
341
352
  currentLanguage,
342
353
  }: ToStarlightSidebarParams): SidebarEntry[] {
343
354
  const starlightEntries: SidebarEntry[] = [];
@@ -350,7 +361,7 @@ export function toStarlightSidebar({
350
361
  type: 'link',
351
362
  href: readmeSlug,
352
363
  label: entry.label,
353
- isCurrent: currentSlug === readmeSlug,
364
+ isCurrent: false,
354
365
  badge: entry.badge,
355
366
  attrs: {
356
367
  'data-stldocs-overview': 'readme',
@@ -369,14 +380,12 @@ export function toStarlightSidebar({
369
380
  language: currentLanguage,
370
381
  });
371
382
 
372
- const isCurrent = resourceOrMethod.data.stainlessPath === currentStainlessPath;
373
-
374
383
  if (resourceOrMethod.data.kind === 'http_method') {
375
384
  starlightEntries.push({
376
385
  type: 'link',
377
386
  href: route,
378
387
  label: entry.label,
379
- isCurrent,
388
+ isCurrent: false,
380
389
  badge: entry.badge,
381
390
  attrs: {
382
391
  'data-stldocs-method': resourceOrMethod.data.httpMethod,
@@ -387,7 +396,7 @@ export function toStarlightSidebar({
387
396
  type: 'link',
388
397
  href: route,
389
398
  label: entry.label,
390
- isCurrent,
399
+ isCurrent: false,
391
400
  badge: entry.badge,
392
401
  attrs: {
393
402
  // TODO: @Ryan: is data.name unique? This is what we used before so I'm not changing, but I am curious.
@@ -398,14 +407,20 @@ export function toStarlightSidebar({
398
407
  throw new Error(`Unknown entry kind ${JSON.stringify(entry)}`);
399
408
  }
400
409
  } else if (entry.kind === 'group') {
410
+ if (entry.resourceGroupKey) {
411
+ const resourceOrMethod = getResourceOrMethod(spec, entry.resourceGroupKey);
412
+ // Skip pushing the group if if the resource it represents is not available in the current language.
413
+ // This occurs when SDK generation for the current language is skipped in the Stainless config for that resource.
414
+ if (resourceOrMethod?.data?.kind === 'resource' && !resourceOrMethod?.data?.[currentLanguage]) {
415
+ continue;
416
+ }
417
+ }
401
418
  starlightEntries.push({
402
419
  type: 'group',
403
420
  label: entry.label,
404
421
  entries: toStarlightSidebar({
405
422
  basePath,
406
- currentSlug,
407
423
  spec,
408
- currentStainlessPath,
409
424
  entries: entry.entries,
410
425
  currentLanguage,
411
426
  }),
@@ -0,0 +1,99 @@
1
+ import crypto from 'crypto';
2
+ import path from 'path';
3
+ import { readdir, readFile, rm, writeFile } from 'fs/promises';
4
+
5
+ type CacheResultSource = 'memory' | 'disk' | 'generation';
6
+
7
+ type CacheGetResult<T> = {
8
+ resultSource: CacheResultSource;
9
+ data: T;
10
+ filePath: string;
11
+ };
12
+
13
+ export class FileCache<Inputs extends Record<string, unknown>, Output> {
14
+ private memoryCache: Map<string, Output> = new Map();
15
+
16
+ private cacheDirectory: string | null = null;
17
+
18
+ public setCacheDirectory(cacheDirectory: string) {
19
+ this.cacheDirectory = cacheDirectory;
20
+ }
21
+ public getCacheDirectory() {
22
+ if (!this.cacheDirectory) {
23
+ console.error(`Tried to retrieve entry from cache, but no cache directory was set.`);
24
+ process.exit(1);
25
+ }
26
+ return this.cacheDirectory;
27
+ }
28
+
29
+ private hashInputs(inputs: Inputs): string {
30
+ return crypto
31
+ .createHash('sha256')
32
+ .update(JSON.stringify(inputs) + this.config.globalHashBits)
33
+ .digest('hex')
34
+ .slice(0, 10);
35
+ }
36
+
37
+ private getFileName(hash: string) {
38
+ return `${hash}.json`;
39
+ }
40
+
41
+ public async cleanupUnusedFiles() {
42
+ const allFiles = await readdir(this.getCacheDirectory());
43
+ const usedFiles = Array.from(this.memoryCache.keys()).map((key) => this.getFileName(key));
44
+ const unusedFiles = allFiles.filter((file) => !usedFiles.includes(file));
45
+ await Promise.all(unusedFiles.map((file) => rm(path.join(this.getCacheDirectory(), file))));
46
+ return {
47
+ deletedCount: unusedFiles.length,
48
+ };
49
+ }
50
+
51
+ public async get(inputs: Inputs): Promise<CacheGetResult<Output>> {
52
+ const hash = this.hashInputs(inputs);
53
+ const filePath = path.join(this.getCacheDirectory(), this.getFileName(hash));
54
+
55
+ const memoryCacheResult = this.memoryCache.get(hash);
56
+ if (memoryCacheResult) {
57
+ return {
58
+ resultSource: 'memory',
59
+ data: memoryCacheResult,
60
+ filePath,
61
+ };
62
+ }
63
+
64
+ const getFromFileOrGenerate = async () => {
65
+ try {
66
+ const fileContents = await readFile(filePath, 'utf8');
67
+ return {
68
+ resultSource: 'disk' as const,
69
+ data: JSON.parse(fileContents) as Output,
70
+ filePath,
71
+ };
72
+ } catch {
73
+ const data = await this.config.generate(inputs);
74
+ await writeFile(filePath, JSON.stringify(data), 'utf8');
75
+ return {
76
+ resultSource: 'generation' as const,
77
+ data,
78
+ filePath,
79
+ };
80
+ }
81
+ };
82
+
83
+ const result = await getFromFileOrGenerate();
84
+ this.memoryCache.set(hash, result.data);
85
+ return result;
86
+ }
87
+
88
+ constructor(
89
+ private config: {
90
+ /**
91
+ * Additional information to include in the hash of the inputs.
92
+ * This is useful for cases where the inputs are the same, but the global state is different.
93
+ * Eg: The preview worker source can be used here.
94
+ */
95
+ globalHashBits: string;
96
+ generate: (inputs: Inputs) => Promise<Output>;
97
+ },
98
+ ) {}
99
+ }
@@ -0,0 +1,27 @@
1
+ import { readFile } from 'fs/promises';
2
+
3
+ import { api } from 'virtual:stainless-apis-manifest';
4
+ import { SpecWithAuth } from './generateSpec';
5
+ import { DocsLanguage } from '@stainless-api/docs-ui/routing';
6
+
7
+ const cachedSpecWithAuth: Record<string, SpecWithAuth> = {};
8
+
9
+ async function getSpecWithAuthInSSR(filePath: string) {
10
+ if (cachedSpecWithAuth[filePath]) {
11
+ return cachedSpecWithAuth[filePath];
12
+ }
13
+ const specStr = await readFile(filePath, 'utf8');
14
+ const json = JSON.parse(specStr) as SpecWithAuth;
15
+ cachedSpecWithAuth[filePath] = json;
16
+ return json;
17
+ }
18
+
19
+ export async function getSDKJSONInSSR(language: DocsLanguage) {
20
+ const filePath = api.languages.find((l) => l.language === language)?.sdkJSONFilePath;
21
+ if (!filePath) {
22
+ throw new Error(`No SDK JSON file path for language: ${language}`);
23
+ }
24
+
25
+ const specWithAuth = await getSpecWithAuthInSSR(filePath);
26
+ return specWithAuth.sdkJson;
27
+ }
@@ -0,0 +1,112 @@
1
+ import type * as SDKJSON from '@stainless/sdk-json';
2
+ import { Languages } from '@stainless-api/docs-ui/routing';
3
+ import { createSDKJSON, ParsedConfig, parseInputs, transformOAS } from './worker';
4
+ import { LanguageGenerateQuery } from '../loadPluginConfig';
5
+ import { FileCache } from './FileCache';
6
+ import previewWorkerCode from '../vendor/preview.worker.docs.js?raw';
7
+
8
+ function getLanguagesFromStainlessConfig(config: ParsedConfig): SDKJSON.SpecLanguage[] {
9
+ // if the Stainless config has a list of docs languages, use that
10
+ if (config.docs?.languages) {
11
+ return config.docs.languages;
12
+ }
13
+
14
+ // otherwise, just loop over all targets in the config + use the ones that are not skipped
15
+ return Object.entries(config.targets)
16
+ .filter(([name, target]) => {
17
+ if (!Languages.includes(name)) return false; // not a valid language
18
+ if (target.skip) return false; // config says to skip this language
19
+ return true;
20
+ })
21
+ .map(([name]) => name) as SDKJSON.SpecLanguage[];
22
+ }
23
+
24
+ // These inputs contain everything needed to generate a spec
25
+ // Combined with the source of the preview workers, we can make a hash to cache the resulting spec
26
+ export type GenerateSpecRawInputs = {
27
+ oasStr: string;
28
+ configStr: string;
29
+ languageOverrides: LanguageGenerateQuery | null;
30
+ stainlessProject: string;
31
+ versionInfo: Record<SDKJSON.SpecLanguage, string> | null;
32
+ };
33
+
34
+ function applyLanguageOverrides(
35
+ initialLanguages: SDKJSON.SpecLanguage[],
36
+ languageOverrides: LanguageGenerateQuery | null,
37
+ ) {
38
+ if (!languageOverrides) return initialLanguages;
39
+ if (languageOverrides.mode === 'exclude') {
40
+ return initialLanguages.filter((language) => !languageOverrides.list.includes(language));
41
+ }
42
+ return languageOverrides.list;
43
+ }
44
+
45
+ async function generateSpecFromStrings({
46
+ oasStr,
47
+ configStr,
48
+ stainlessProject,
49
+ languageOverrides,
50
+ versionInfo,
51
+ }: GenerateSpecRawInputs) {
52
+ const { oas, config } = await parseInputs({
53
+ oas: oasStr,
54
+ config: configStr,
55
+ });
56
+
57
+ const transformedOAS = await transformOAS({ oas, config });
58
+
59
+ let languagesToGenerate = getLanguagesFromStainlessConfig(config);
60
+ // by default, we should generate the HTTP spec (unless it's explicitly excluded)
61
+ if (!languagesToGenerate.includes('http')) {
62
+ languagesToGenerate.push('http');
63
+ }
64
+
65
+ languagesToGenerate = applyLanguageOverrides(languagesToGenerate, languageOverrides);
66
+
67
+ // SDKJSON has weird behavior where it will create a spec with HTTP, even if it's not in the languages list
68
+ const sdkJson = await createSDKJSON({
69
+ oas: transformedOAS,
70
+ config,
71
+ // if language overrides are provided, use them, otherwise use the languages from the Stainless config
72
+ languages: languagesToGenerate,
73
+ projectName: stainlessProject,
74
+ });
75
+
76
+ let languages = sdkJson.docs?.languages;
77
+
78
+ if (!languages) {
79
+ throw new Error(`SDKJSON created without any languages`);
80
+ }
81
+
82
+ // if language overrides are provided, filter the languages to only include the ones that are in the overrides
83
+ languages = languages.filter((language) => languagesToGenerate.includes(language));
84
+
85
+ if (versionInfo) {
86
+ for (const [lang, version] of Object.entries(versionInfo)) {
87
+ const meta = sdkJson.metadata[lang as SDKJSON.SpecLanguage];
88
+ if (meta?.version) meta.version = version;
89
+ }
90
+ }
91
+
92
+ const opts = Object.entries(config.client_settings.opts).map(([k, v]) => ({ name: k, ...v }));
93
+ return {
94
+ sdkJson,
95
+ languages,
96
+ auth: sdkJson.security_schemes.map((scheme) => ({
97
+ ...scheme,
98
+ opts: opts.filter((opt) => opt.auth?.security_scheme === scheme.name),
99
+ })),
100
+ };
101
+ }
102
+
103
+ export const specCache = new FileCache({
104
+ generate: generateSpecFromStrings,
105
+ globalHashBits: previewWorkerCode, // you can change this as a last resort to invalidate the cache
106
+ });
107
+
108
+ export type SpecCacheResult = Awaited<ReturnType<typeof specCache.get>>;
109
+
110
+ export type GenerateSpecFn = typeof generateSpecFromStrings;
111
+
112
+ export type SpecWithAuth = Awaited<ReturnType<GenerateSpecFn>>;
@@ -0,0 +1,132 @@
1
+ import { mkdir } from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ import type * as VirtualManifestModule from 'virtual:stainless-apis-manifest';
5
+
6
+ import { makeAsyncVirtualModPlugin } from '../../shared/virtualModule';
7
+
8
+ import { NormalizedStainlessStarlightConfig } from '../loadPluginConfig';
9
+
10
+ import { specCache, SpecCacheResult } from './generateSpec';
11
+ import { AstroIntegrationLogger } from 'astro';
12
+ import type * as SDKJSON from '@stainless/sdk-json';
13
+
14
+ /**
15
+ * A helper class to manage multiple spec cache results for a single API
16
+ * An API may have multiple spec cache results if it has multiple languages
17
+ * Note that one spec may contain multiple languages.
18
+ * */
19
+ export class SpecComposite {
20
+ private languages: Set<SDKJSON.SpecLanguage>;
21
+ private readonly specs: Partial<Record<SDKJSON.SpecLanguage, SpecCacheResult>>;
22
+
23
+ public getLanguages() {
24
+ return Array.from(this.languages);
25
+ }
26
+
27
+ public getByLanguage(language: SDKJSON.SpecLanguage) {
28
+ const spec = this.specs[language];
29
+ if (!spec) {
30
+ throw new Error(`Spec for language ${language} not found`);
31
+ }
32
+ return spec;
33
+ }
34
+
35
+ /**
36
+ * Returns all specs. It will return each spec once, even if it has multiple languages.
37
+ * */
38
+ public listUniqueSpecs() {
39
+ const seen = new Set<SpecCacheResult>();
40
+ const unique: SpecCacheResult[] = [];
41
+ for (const spec of Object.values(this.specs)) {
42
+ if (!seen.has(spec)) {
43
+ seen.add(spec);
44
+ unique.push(spec);
45
+ }
46
+ }
47
+ return unique;
48
+ }
49
+
50
+ public listAllLanguagesAndIncludeSpecs() {
51
+ return this.getLanguages().map((language) => ({
52
+ language,
53
+ spec: this.getByLanguage(language),
54
+ }));
55
+ }
56
+
57
+ constructor(specs: SpecCacheResult[]) {
58
+ this.languages = new Set<SDKJSON.SpecLanguage>();
59
+ this.specs = {};
60
+ for (const spec of specs) {
61
+ for (const lang of spec.data.languages) {
62
+ if (this.languages.has(lang)) {
63
+ throw new Error(`Language appears multiple times in the same API: ${lang}`);
64
+ }
65
+ if (lang === 'openapi' || lang === 'sql') continue;
66
+ this.languages.add(lang);
67
+ this.specs[lang] = spec;
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ /** Runs once in the build process */
74
+ export async function startSpecLoader(
75
+ pluginConfig: NormalizedStainlessStarlightConfig,
76
+ logger: AstroIntegrationLogger,
77
+ codegenDir: URL,
78
+ ) {
79
+ const specsDirectory = path.join(codegenDir.pathname, 'specs');
80
+ await mkdir(specsDirectory, { recursive: true });
81
+
82
+ logger.debug(`Setting cache directory to ${specsDirectory}`);
83
+
84
+ // 🚨 Important! You cannot call loadSpecs() before setting the cache directory.
85
+ specCache.setCacheDirectory(specsDirectory);
86
+
87
+ async function load() {
88
+ const specs = await pluginConfig.api.loadSpecs();
89
+
90
+ // not awaited since it's just cleanup
91
+ specCache
92
+ .cleanupUnusedFiles()
93
+ .then((result) => {
94
+ if (result.deletedCount > 0) {
95
+ logger.info(`Cleaned up ${result.deletedCount} unused spec files`);
96
+ } else {
97
+ logger.debug(`No unused spec files to clean up`);
98
+ }
99
+ })
100
+ .catch(() => {
101
+ logger.warn(`Failed to clean up unused spec files`);
102
+ });
103
+
104
+ return {
105
+ specComposite: new SpecComposite(specs),
106
+ };
107
+ }
108
+
109
+ const specPromise = load();
110
+
111
+ return {
112
+ specPromise,
113
+ // this virtual module only resolves when the spec is generated
114
+ // this prevents the SSR module from trying to read the spec file before it's generated
115
+ vitePlugins: [
116
+ makeAsyncVirtualModPlugin<typeof VirtualManifestModule>('virtual:stainless-apis-manifest', async () => {
117
+ const api = await specPromise;
118
+
119
+ return {
120
+ api: {
121
+ languages: api.specComposite.listAllLanguagesAndIncludeSpecs().map((langSpec) => ({
122
+ language: langSpec.language,
123
+ sdkJSONFilePath: langSpec.spec.filePath,
124
+ })),
125
+ },
126
+ };
127
+ }),
128
+ ],
129
+ };
130
+ }
131
+
132
+ export type SpecLoader = Awaited<ReturnType<typeof startSpecLoader>>;