@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.
- package/CHANGELOG.md +1102 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +90 -0
- package/{eslint.config.js → eslint.config.ts} +0 -2
- package/locals.d.ts +17 -0
- package/package.json +62 -44
- package/playground-virtual-modules.d.ts +96 -0
- package/plugin/assets/languages/cli.svg +14 -0
- package/plugin/assets/languages/csharp.svg +1 -0
- package/plugin/assets/languages/php.svg +4 -0
- package/plugin/buildAlgoliaIndex.ts +40 -39
- package/plugin/components/MethodDescription.tsx +54 -0
- package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
- package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
- package/plugin/components/RequestBuilder/index.tsx +40 -0
- package/plugin/components/RequestBuilder/props.ts +9 -0
- package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
- package/plugin/components/RequestBuilder/styles.css +67 -0
- package/plugin/components/SDKSelect.astro +18 -111
- package/plugin/components/SnippetCode.tsx +112 -70
- package/plugin/components/StainlessIslands.tsx +126 -0
- package/plugin/components/search/SearchAlgolia.astro +46 -29
- package/plugin/components/search/SearchIsland.tsx +61 -37
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/globalJs/ai-dropdown-options.ts +248 -0
- package/plugin/globalJs/code-snippets.ts +45 -16
- package/plugin/globalJs/copy.ts +115 -27
- package/plugin/globalJs/create-playground.shim.ts +3 -0
- package/plugin/globalJs/method-descriptions.ts +33 -0
- package/plugin/globalJs/navigation.ts +24 -44
- package/plugin/globalJs/playground-data.shim.ts +1 -0
- package/plugin/globalJs/playground-data.ts +14 -0
- package/plugin/globalJs/summary-selection-tweak.ts +29 -0
- package/plugin/helpers/generateDocsRoutes.ts +59 -0
- package/plugin/helpers/multiSpec.ts +8 -0
- package/plugin/index.ts +317 -141
- package/plugin/languages.ts +8 -2
- package/plugin/loadPluginConfig.ts +284 -109
- package/plugin/markdown/highlighter.ts +100 -0
- package/plugin/markdown/index.ts +39 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
- package/plugin/react/Routing.tsx +98 -263
- package/plugin/referencePlaceholderUtils.ts +17 -14
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +39 -35
- package/plugin/routes/Docs.astro +72 -111
- package/plugin/routes/DocsStatic.astro +6 -5
- package/plugin/routes/Overview.astro +46 -22
- package/plugin/routes/llms.ts +186 -0
- package/plugin/routes/markdown.ts +13 -12
- package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +84 -69
- package/plugin/specs/FileCache.ts +99 -0
- package/plugin/specs/fetchSpecSSR.ts +27 -0
- package/plugin/specs/generateSpec.ts +112 -0
- package/plugin/specs/index.ts +132 -0
- package/plugin/specs/inputResolver.ts +148 -0
- package/plugin/{cms → specs}/worker.ts +82 -5
- package/plugin/vendor/preview.worker.docs.js +27121 -16890
- package/plugin/vendor/templates/cli.md +1 -0
- package/plugin/vendor/templates/go.md +4 -2
- package/plugin/vendor/templates/java.md +5 -1
- package/plugin/vendor/templates/kotlin.md +5 -1
- package/plugin/vendor/templates/node.md +4 -2
- package/plugin/vendor/templates/python.md +4 -2
- package/plugin/vendor/templates/ruby.md +4 -2
- package/plugin/vendor/templates/terraform.md +1 -1
- package/plugin/vendor/templates/typescript.md +3 -1
- package/resolveSrcFile.ts +10 -0
- package/scripts/vendor_deps.ts +5 -5
- package/shared/conditionalIntegration.ts +28 -0
- package/shared/getProsePages.ts +41 -0
- package/shared/getSharedLogger.ts +15 -0
- package/shared/terminalUtils.ts +3 -0
- package/shared/virtualModule.ts +46 -1
- package/src/content.config.ts +9 -0
- package/stl-docs/aiChatExamples.ts +95 -0
- package/stl-docs/chat/docs-chat-handler.ts +18 -0
- package/stl-docs/chat/hook.ts +215 -0
- package/stl-docs/chat/schemas.ts +70 -0
- package/stl-docs/chat/stainless-handler/index.ts +126 -0
- package/stl-docs/chat/stream-util.ts +16 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +188 -0
- package/stl-docs/chat/ui/Trigger.tsx +154 -0
- package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
- package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
- package/stl-docs/chat/ui/components/ChatLog.tsx +96 -0
- package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
- package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +109 -0
- package/stl-docs/chat/ui/components/Table.tsx +15 -0
- package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
- package/stl-docs/chat/ui/components/hljs-github.css +81 -0
- package/stl-docs/chat/ui/scroll-manager.ts +86 -0
- package/stl-docs/chat/ui/types.ts +45 -0
- package/stl-docs/components/AIDropdown.tsx +63 -0
- package/stl-docs/components/AiChatIsland.tsx +16 -0
- package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
- package/stl-docs/components/ContentPanel.astro +9 -0
- package/stl-docs/components/Footer.astro +89 -0
- package/stl-docs/components/Head.astro +20 -0
- package/stl-docs/components/Header.astro +3 -9
- package/stl-docs/components/PageFrame.astro +37 -0
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/PageTitle.astro +82 -0
- package/stl-docs/components/StainlessLogo.svg +4 -0
- package/stl-docs/components/ThemeProvider.astro +36 -0
- package/stl-docs/components/ThemeSelect.astro +84 -146
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/headers/DefaultHeader.astro +6 -8
- package/stl-docs/components/headers/StackedHeader.astro +10 -53
- package/stl-docs/components/icons/chat-gpt.tsx +2 -2
- package/stl-docs/components/icons/cursor.tsx +10 -0
- package/stl-docs/components/icons/gemini.tsx +19 -0
- package/stl-docs/components/icons/markdown.tsx +1 -1
- package/stl-docs/components/index.ts +1 -0
- package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
- package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
- package/stl-docs/components/mintlify-compat/Frame.astro +6 -6
- package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
- package/stl-docs/components/mintlify-compat/card.css +4 -4
- package/stl-docs/components/mintlify-compat/index.ts +2 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +38 -77
- package/stl-docs/components/nav-tabs/NavTabs.astro +81 -81
- package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +1 -2
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +5 -2
- package/stl-docs/components/pagination/HomeLink.astro +10 -0
- package/stl-docs/components/pagination/Pagination.astro +177 -0
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
- package/stl-docs/components/pagination/util.ts +71 -0
- package/stl-docs/components/scripts.ts +1 -0
- package/stl-docs/components/sidebars/BaseSidebar.astro +80 -2
- package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
- package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
- package/stl-docs/disableCalloutSyntax.ts +36 -0
- package/stl-docs/fonts.ts +186 -0
- package/stl-docs/index.ts +176 -58
- package/stl-docs/loadStlDocsConfig.ts +73 -8
- package/stl-docs/proseDocSync.test.ts +74 -0
- package/stl-docs/proseDocSync.ts +344 -0
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
- package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
- package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
- package/stl-docs/proseSearchIndexing.ts +218 -0
- package/stl-docs/tabsMiddleware.ts +14 -5
- package/styles/code.css +53 -49
- package/styles/links.css +2 -37
- package/styles/method-descriptions.css +36 -0
- package/styles/overrides.css +28 -46
- package/styles/page.css +228 -38
- package/styles/sdk_select.css +9 -6
- package/styles/search.css +11 -21
- package/styles/sidebar.css +28 -215
- package/styles/{variables.css → sl-variables.css} +4 -8
- package/styles/stldocs-variables.css +6 -0
- package/styles/toc.css +19 -8
- package/theme.css +11 -9
- package/tsconfig.json +1 -4
- package/virtual-module.d.ts +66 -8
- package/components/variables.css +0 -112
- package/plugin/cms/client.ts +0 -62
- package/plugin/cms/server.ts +0 -268
- package/plugin/globalJs/ai-dropdown.ts +0 -57
- package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
- package/stl-docs/components/ClientRouterHead.astro +0 -41
- package/stl-docs/components/content-panel/ContentPanel.astro +0 -69
- package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
- package/stl-docs/components/mintlify-compat/Step.astro +0 -56
- package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
- package/styles/fonts.css +0 -68
- /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import type * as SDKJSON from '
|
|
2
|
-
import { generateRoute, walkTree, type DocsLanguage } from '@stainless-api/docs-ui/
|
|
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,
|
|
123
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
277
|
+
const { resources, sharedModelsResource } = pullOutSharedModelsResource(Object.values(resourceMap ?? {}));
|
|
257
278
|
|
|
258
|
-
|
|
259
|
-
|
|
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 === '
|
|
298
|
-
|
|
315
|
+
if (entry.data.kind === 'model') {
|
|
316
|
+
continue;
|
|
299
317
|
}
|
|
300
|
-
if (entry.data
|
|
318
|
+
if (makeMethodOrResourceKey(entry.data) === endpointOrConfigRef) {
|
|
301
319
|
return entry;
|
|
302
320
|
}
|
|
303
321
|
}
|
|
304
322
|
return null;
|
|
305
323
|
}
|
|
306
324
|
|
|
307
|
-
|
|
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
|
-
|
|
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:
|
|
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>>;
|