@stainless-api/docs 0.1.0-beta.11 → 0.1.0-beta.110

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 (149) hide show
  1. package/CHANGELOG.md +919 -0
  2. package/eslint-suppressions.json +27 -0
  3. package/locals.d.ts +17 -0
  4. package/package.json +50 -41
  5. package/playground-virtual-modules.d.ts +96 -0
  6. package/plugin/assets/languages/cli.svg +14 -0
  7. package/plugin/assets/languages/csharp.svg +1 -0
  8. package/plugin/assets/languages/php.svg +4 -0
  9. package/plugin/buildAlgoliaIndex.ts +40 -39
  10. package/plugin/components/MethodDescription.tsx +54 -0
  11. package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
  12. package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
  13. package/plugin/components/RequestBuilder/index.tsx +37 -0
  14. package/plugin/components/RequestBuilder/props.ts +9 -0
  15. package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
  16. package/plugin/components/RequestBuilder/styles.css +67 -0
  17. package/plugin/components/SDKSelect.astro +18 -111
  18. package/plugin/components/SnippetCode.tsx +110 -68
  19. package/plugin/components/StainlessIslands.tsx +126 -0
  20. package/plugin/components/search/SearchAlgolia.astro +46 -29
  21. package/plugin/components/search/SearchIsland.tsx +47 -29
  22. package/plugin/generateAPIReferenceLink.ts +2 -2
  23. package/plugin/globalJs/ai-dropdown-options.ts +243 -0
  24. package/plugin/globalJs/code-snippets.ts +40 -11
  25. package/plugin/globalJs/copy.ts +95 -17
  26. package/plugin/globalJs/create-playground.shim.ts +3 -0
  27. package/plugin/globalJs/method-descriptions.ts +33 -0
  28. package/plugin/globalJs/navigation.ts +12 -32
  29. package/plugin/globalJs/playground-data.shim.ts +1 -0
  30. package/plugin/globalJs/playground-data.ts +14 -0
  31. package/plugin/helpers/generateDocsRoutes.ts +59 -0
  32. package/plugin/helpers/multiSpec.ts +8 -0
  33. package/plugin/index.ts +304 -138
  34. package/plugin/languages.ts +8 -2
  35. package/plugin/loadPluginConfig.ts +251 -107
  36. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
  37. package/plugin/react/Routing.tsx +212 -141
  38. package/plugin/referencePlaceholderUtils.ts +18 -15
  39. package/plugin/replaceSidebarPlaceholderMiddleware.ts +38 -34
  40. package/plugin/routes/Docs.astro +70 -111
  41. package/plugin/routes/DocsStatic.astro +6 -5
  42. package/plugin/routes/Overview.astro +45 -21
  43. package/plugin/routes/markdown.ts +13 -12
  44. package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +49 -60
  45. package/plugin/specs/FileCache.ts +99 -0
  46. package/plugin/specs/fetchSpecSSR.ts +27 -0
  47. package/plugin/specs/generateSpec.ts +112 -0
  48. package/plugin/specs/index.ts +132 -0
  49. package/plugin/specs/inputResolver.ts +146 -0
  50. package/plugin/{cms → specs}/worker.ts +82 -5
  51. package/plugin/vendor/preview.worker.docs.js +27303 -18260
  52. package/plugin/vendor/templates/cli.md +1 -0
  53. package/plugin/vendor/templates/go.md +4 -2
  54. package/plugin/vendor/templates/java.md +5 -1
  55. package/plugin/vendor/templates/kotlin.md +5 -1
  56. package/plugin/vendor/templates/node.md +4 -2
  57. package/plugin/vendor/templates/python.md +4 -2
  58. package/plugin/vendor/templates/ruby.md +4 -2
  59. package/plugin/vendor/templates/terraform.md +1 -1
  60. package/plugin/vendor/templates/typescript.md +3 -1
  61. package/resolveSrcFile.ts +10 -0
  62. package/scripts/vendor_deps.ts +5 -5
  63. package/shared/conditionalIntegration.ts +28 -0
  64. package/shared/getProsePages.ts +41 -0
  65. package/shared/getSharedLogger.ts +15 -0
  66. package/shared/terminalUtils.ts +3 -0
  67. package/shared/virtualModule.ts +54 -1
  68. package/src/content.config.ts +9 -0
  69. package/stl-docs/components/AIDropdown.tsx +63 -0
  70. package/stl-docs/components/AiChatIsland.tsx +14 -0
  71. package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
  72. package/stl-docs/components/Footer.astro +89 -0
  73. package/stl-docs/components/Head.astro +20 -0
  74. package/stl-docs/components/Header.astro +3 -10
  75. package/stl-docs/components/PageFrame.astro +34 -0
  76. package/stl-docs/components/PageSidebar.astro +11 -0
  77. package/stl-docs/components/PageTitle.astro +82 -0
  78. package/stl-docs/components/StainlessLogo.svg +4 -0
  79. package/stl-docs/components/TableOfContents.astro +34 -0
  80. package/stl-docs/components/ThemeProvider.astro +36 -0
  81. package/stl-docs/components/ThemeSelect.astro +84 -146
  82. package/stl-docs/components/TwoColumnContent.astro +2 -0
  83. package/stl-docs/components/content-panel/ContentPanel.astro +4 -64
  84. package/stl-docs/components/headers/DefaultHeader.astro +4 -6
  85. package/stl-docs/components/headers/StackedHeader.astro +8 -51
  86. package/stl-docs/components/icons/chat-gpt.tsx +2 -2
  87. package/stl-docs/components/icons/cursor.tsx +10 -0
  88. package/stl-docs/components/icons/gemini.tsx +19 -0
  89. package/stl-docs/components/icons/markdown.tsx +1 -1
  90. package/stl-docs/components/index.ts +1 -0
  91. package/stl-docs/components/mintlify-compat/Frame.astro +4 -4
  92. package/stl-docs/components/mintlify-compat/card.css +4 -4
  93. package/stl-docs/components/mintlify-compat/index.ts +2 -4
  94. package/stl-docs/components/nav-tabs/NavDropdown.astro +31 -75
  95. package/stl-docs/components/nav-tabs/NavTabs.astro +79 -81
  96. package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +15 -7
  97. package/stl-docs/components/nav-tabs/buildNavLinks.ts +3 -2
  98. package/stl-docs/components/pagination/HomeLink.astro +10 -0
  99. package/stl-docs/components/pagination/Pagination.astro +177 -0
  100. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
  101. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
  102. package/stl-docs/components/pagination/util.ts +71 -0
  103. package/stl-docs/components/scripts.ts +1 -0
  104. package/stl-docs/components/sidebars/BaseSidebar.astro +79 -2
  105. package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
  106. package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
  107. package/stl-docs/disableCalloutSyntax.ts +36 -0
  108. package/stl-docs/fonts.ts +186 -0
  109. package/stl-docs/index.ts +171 -51
  110. package/stl-docs/loadStlDocsConfig.ts +64 -8
  111. package/stl-docs/proseDocSync.ts +314 -0
  112. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
  113. package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
  114. package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
  115. package/stl-docs/proseSearchIndexing.ts +222 -0
  116. package/stl-docs/tabsMiddleware.ts +13 -4
  117. package/styles/code.css +53 -49
  118. package/styles/links.css +2 -37
  119. package/styles/method-descriptions.css +36 -0
  120. package/styles/overrides.css +28 -46
  121. package/styles/page.css +230 -52
  122. package/styles/sdk_select.css +9 -6
  123. package/styles/search.css +11 -21
  124. package/styles/sidebar.css +25 -211
  125. package/styles/{variables.css → sl-variables.css} +4 -8
  126. package/styles/stldocs-variables.css +6 -0
  127. package/styles/toc.css +19 -8
  128. package/theme.css +11 -9
  129. package/tsconfig.json +1 -4
  130. package/virtual-module.d.ts +65 -8
  131. package/components/variables.css +0 -112
  132. package/plugin/cms/client.ts +0 -62
  133. package/plugin/cms/server.ts +0 -268
  134. package/plugin/globalJs/ai-dropdown.ts +0 -57
  135. package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
  136. package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
  137. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
  138. package/stl-docs/components/mintlify-compat/Step.astro +0 -56
  139. package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
  140. package/styles/fonts.css +0 -68
  141. /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
  142. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
  143. /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
  144. /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
  145. /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
  146. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
  147. /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
  148. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
  149. /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
@@ -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
+ export 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, ResolvedAPIConfigEntry } 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
+ export type LoadedAPIConfigEntry = Omit<ResolvedAPIConfigEntry, 'loadSpecs'> & {
15
+ specs: SpecCacheResult[];
16
+ languages: SDKJSON.SpecLanguage[];
17
+ };
18
+
19
+ /**
20
+ * A helper class to manage multiple spec cache results for a single API
21
+ * An API may have multiple spec cache results if it has multiple languages
22
+ * Note that one spec may contain multiple languages.
23
+ * */
24
+ export class SpecComposite {
25
+ private languages: Set<SDKJSON.SpecLanguage>;
26
+ private readonly specs: Partial<Record<SDKJSON.SpecLanguage, SpecCacheResult>>;
27
+
28
+ public getLanguages() {
29
+ return Array.from(this.languages);
30
+ }
31
+
32
+ public getByLanguage(language: SDKJSON.SpecLanguage) {
33
+ const spec = this.specs[language];
34
+ if (!spec) {
35
+ throw new Error(`Spec for language ${language} not found`);
36
+ }
37
+ return spec;
38
+ }
39
+
40
+ /**
41
+ * Returns all specs. It will return each spec once, even if it has multiple languages.
42
+ * */
43
+ public listUniqueSpecs() {
44
+ const seen = new Set<SpecCacheResult>();
45
+ const unique: SpecCacheResult[] = [];
46
+ for (const spec of Object.values(this.specs)) {
47
+ if (!seen.has(spec)) {
48
+ seen.add(spec);
49
+ unique.push(spec);
50
+ }
51
+ }
52
+ return unique;
53
+ }
54
+
55
+ public listAllLanguagesAndIncludeSpecs() {
56
+ return this.getLanguages().map((language) => ({
57
+ language,
58
+ spec: this.getByLanguage(language),
59
+ }));
60
+ }
61
+
62
+ constructor(specs: SpecCacheResult[]) {
63
+ this.languages = new Set<SDKJSON.SpecLanguage>();
64
+ this.specs = {};
65
+ for (const spec of specs) {
66
+ for (const lang of spec.data.languages) {
67
+ if (this.languages.has(lang)) {
68
+ throw new Error(`Language appears multiple times in the same API: ${lang}`);
69
+ }
70
+ if (lang === 'openapi' || lang === 'sql') continue;
71
+ this.languages.add(lang);
72
+ this.specs[lang] = spec;
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ /** Runs once in the build process */
79
+ export async function startSpecLoader(
80
+ pluginConfig: NormalizedStainlessStarlightConfig,
81
+ logger: AstroIntegrationLogger,
82
+ codegenDir: URL,
83
+ ) {
84
+ const specsDirectory = path.join(codegenDir.pathname, 'specs');
85
+ await mkdir(specsDirectory, { recursive: true });
86
+
87
+ logger.debug(`Setting cache directory to ${specsDirectory}`);
88
+
89
+ // 🚨 Important! You cannot call loadSpecs() before setting the cache directory.
90
+ specCache.setCacheDirectory(specsDirectory);
91
+
92
+ async function load() {
93
+ const specs = await pluginConfig.api.loadSpecs();
94
+
95
+ // not awaited since it's just cleanup
96
+ specCache.cleanupUnusedFiles().then((result) => {
97
+ if (result.deletedCount > 0) {
98
+ logger.info(`Cleaned up ${result.deletedCount} unused spec files`);
99
+ } else {
100
+ logger.debug(`No unused spec files to clean up`);
101
+ }
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>>;
@@ -0,0 +1,146 @@
1
+ import { DocsLanguage } from '@stainless-api/docs-ui/routing';
2
+ import { readFile } from 'fs/promises';
3
+ import { AstroIntegrationLogger } from 'astro';
4
+ import { bold } from '../../shared/terminalUtils';
5
+ import type { LanguageGenerateQuery, LoadedApiKey } from '../loadPluginConfig';
6
+ import Stainless, { APIError } from '@stainless-api/sdk';
7
+ import { GenerateSpecRawInputs } from './generateSpec';
8
+
9
+ export type SpecInputResolver = {
10
+ resolve: (context: { apiKey: LoadedApiKey | null }) => Promise<GenerateSpecRawInputs>;
11
+ printError: (error: Error, logger: AstroIntegrationLogger) => void;
12
+ };
13
+
14
+ function fromFiles({
15
+ oasPath,
16
+ configPath,
17
+ languageOverrides,
18
+ stainlessProject,
19
+ }: {
20
+ oasPath: string;
21
+ configPath: string;
22
+ languageOverrides: LanguageGenerateQuery | null;
23
+ stainlessProject: string;
24
+ }): SpecInputResolver {
25
+ return {
26
+ resolve: async () => {
27
+ const oasStr = await readFile(oasPath, 'utf8');
28
+ const configStr = await readFile(configPath, 'utf8');
29
+ return {
30
+ oasStr,
31
+ configStr,
32
+ versionInfo: null,
33
+ languageOverrides,
34
+ stainlessProject,
35
+ };
36
+ },
37
+ printError: (error: Error, logger: AstroIntegrationLogger) => {
38
+ logger.error(bold('Failed to resolve spec inputs from files:'));
39
+ logger.error(error.message);
40
+ },
41
+ };
42
+ }
43
+
44
+ async function fetchVersionInfo(project: string, apiKey: string): Promise<Record<DocsLanguage, string>> {
45
+ const data = await fetch(`https://api.stainless.com/api/projects/${project}/package-versions`, {
46
+ headers: { Authorization: `Bearer ${apiKey}` },
47
+ });
48
+
49
+ const content = await data.text();
50
+ return JSON.parse(content) as Record<DocsLanguage, string>;
51
+ }
52
+
53
+ function redactApiKey(apiKey: string) {
54
+ return apiKey
55
+ .split('')
56
+ .map((char, index) => (index < 10 ? char : '*'))
57
+ .join('');
58
+ }
59
+
60
+ function fromStainlessApi(inputs: {
61
+ stainlessProject: string;
62
+ branch: string;
63
+ apiKey?: LoadedApiKey;
64
+ languageOverrides: LanguageGenerateQuery | null;
65
+ }): SpecInputResolver {
66
+ let apiKey: string | undefined;
67
+ return {
68
+ async resolve(context) {
69
+ apiKey = context.apiKey?.value ?? inputs.apiKey?.value;
70
+
71
+ if (!apiKey) {
72
+ throw new Error('No API key provided');
73
+ }
74
+
75
+ const client = new Stainless({ apiKey });
76
+ const configs = await client.projects.configs.retrieve({
77
+ project: inputs.stainlessProject,
78
+ branch: inputs.branch,
79
+ include: 'openapi',
80
+ });
81
+ const versionInfo = await fetchVersionInfo(inputs.stainlessProject, apiKey);
82
+
83
+ const configYML = Object.values(configs)[0] as { content: unknown };
84
+ const oasJson = Object.values(configs)[1] as { content: unknown };
85
+ const oasStr = oasJson['content'];
86
+ const configStr = configYML['content'];
87
+
88
+ if (typeof oasStr !== 'string' || typeof configStr !== 'string') {
89
+ throw new Error('Received invalid OAS or config from Stainless');
90
+ }
91
+
92
+ return {
93
+ oasStr,
94
+ configStr,
95
+ versionInfo,
96
+ languageOverrides: inputs.languageOverrides,
97
+ stainlessProject: inputs.stainlessProject,
98
+ };
99
+ },
100
+ printError: (error: Error, logger: AstroIntegrationLogger) => {
101
+ if (error instanceof APIError && error.status >= 400 && error.status < 500) {
102
+ logger.error(`Requested project slug: "${inputs.stainlessProject}"`);
103
+ if (apiKey) {
104
+ logger.error(`API key: "${redactApiKey(apiKey)}"`);
105
+ }
106
+ logger.error(
107
+ `This error can usually be corrected by re-authenticating with the Stainless. Use the CLI (stl auth login) or verify that the Stainless API key you're using can access the project mentioned above.`,
108
+ );
109
+ }
110
+ },
111
+ };
112
+ }
113
+
114
+ function fromStrings({
115
+ oasStr,
116
+ configStr,
117
+ languageOverrides,
118
+ stainlessProject,
119
+ }: {
120
+ oasStr: string;
121
+ configStr: string;
122
+ languageOverrides: LanguageGenerateQuery | null;
123
+ stainlessProject: string;
124
+ }): SpecInputResolver {
125
+ return {
126
+ async resolve() {
127
+ return {
128
+ oasStr,
129
+ configStr,
130
+ versionInfo: null,
131
+ languageOverrides,
132
+ stainlessProject,
133
+ };
134
+ },
135
+ printError(error: Error, logger: AstroIntegrationLogger) {
136
+ logger.error(bold('Failed to resolve spec inputs from strings:'));
137
+ logger.error(error.message);
138
+ },
139
+ };
140
+ }
141
+
142
+ export const resolveSpec = {
143
+ fromFiles,
144
+ fromStainlessApi,
145
+ fromStrings,
146
+ } satisfies Record<string, (...args: never[]) => SpecInputResolver>;
@@ -1,6 +1,6 @@
1
1
  import Worker from 'web-worker';
2
- import { Languages, type DocsLanguage } from '@stainless-api/docs-ui/src/routing';
3
- import type * as SDKJSON from '~/lib/json-spec-v2/types';
2
+ import { Languages, type DocsLanguage } from '@stainless-api/docs-ui/routing';
3
+ import type * as SDKJSON from '@stainless/sdk-json';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { dirname, resolve } from 'node:path';
6
6
  import fs from 'fs/promises';
@@ -12,7 +12,81 @@ const __dirname = dirname(__filename);
12
12
  const workerPath = resolve(__dirname, '..', 'vendor', 'preview.worker.docs.js');
13
13
 
14
14
  type OpenAPIDocument = Record<string, any>;
15
- type ParsedConfig = Record<string, any>;
15
+ export type ParsedConfig = {
16
+ docs:
17
+ | {
18
+ title?: string | undefined;
19
+ favicon?: string | undefined;
20
+ logo_icon?: string | undefined;
21
+ search?:
22
+ | {
23
+ algolia?:
24
+ | {
25
+ app_id: string;
26
+ index_name: string;
27
+ search_key: string;
28
+ }
29
+ | undefined;
30
+ }
31
+ | undefined;
32
+ description?: string | undefined;
33
+ languages?:
34
+ | ('node' | 'typescript' | 'python' | 'java' | 'kotlin' | 'go' | 'ruby' | 'terraform' | 'http')[]
35
+ | undefined;
36
+ snippets?:
37
+ | {
38
+ exclude_languages?: string[] | undefined;
39
+ }
40
+ | undefined;
41
+ show_security?: boolean | undefined;
42
+ show_readme?: boolean | undefined;
43
+ base_path?: string | undefined;
44
+ navigation?:
45
+ | {
46
+ menubar?:
47
+ | {
48
+ title: string;
49
+ icon?: string;
50
+ variant?: string;
51
+ href?: string | undefined;
52
+ page?: string | undefined;
53
+ }[]
54
+ | undefined;
55
+ sidebar?:
56
+ | {
57
+ title: string;
58
+ icon?: string;
59
+ variant?: string;
60
+ href?: string | undefined;
61
+ page?: string | undefined;
62
+ }[]
63
+ | undefined;
64
+ }
65
+ | undefined;
66
+ pages?: unknown;
67
+ resources?: unknown[] | undefined;
68
+ }
69
+ | undefined;
70
+ targets: Record<string, { skip?: boolean }>;
71
+ client_settings: {
72
+ opts: {
73
+ [x: string]: {
74
+ type: 'string' | 'number' | 'boolean' | 'null' | 'integer';
75
+ nullable: boolean;
76
+ description?: string | undefined;
77
+ example?: unknown;
78
+ default?: unknown;
79
+ read_env?: string | undefined;
80
+ auth?:
81
+ | {
82
+ security_scheme: string;
83
+ role?: 'value' | 'password' | 'username' | 'client_id' | 'client_secret' | undefined;
84
+ }
85
+ | undefined;
86
+ };
87
+ };
88
+ };
89
+ };
16
90
 
17
91
  function runJob({ type, signal, data }: { type: string; signal?: AbortSignal; data: any }) {
18
92
  const stainlessWorker = new Worker(workerPath, {
@@ -22,6 +96,7 @@ function runJob({ type, signal, data }: { type: string; signal?: AbortSignal; da
22
96
 
23
97
  return new Promise<any>((resolve, reject) => {
24
98
  stainlessWorker.addEventListener('error', (e) => {
99
+ e.preventDefault();
25
100
  reject(e);
26
101
  });
27
102
 
@@ -85,10 +160,12 @@ export async function createSDKJSON({
85
160
  oas,
86
161
  config,
87
162
  languages,
163
+ projectName,
88
164
  }: {
89
165
  oas: OpenAPIDocument;
90
166
  config: ParsedConfig;
91
167
  languages: DocsLanguage[];
168
+ projectName: string;
92
169
  }) {
93
170
  const templatePath = resolve(__dirname, '../vendor/templates');
94
171
  const readmeLoader = await Promise.all(
@@ -98,7 +175,7 @@ export async function createSDKJSON({
98
175
  try {
99
176
  const content = await fs.readFile(mdfile);
100
177
  return [language, content.toString()];
101
- } catch (err) {
178
+ } catch {
102
179
  return [language, null];
103
180
  }
104
181
  }),
@@ -113,7 +190,7 @@ export async function createSDKJSON({
113
190
  config,
114
191
  languages,
115
192
  transform: false,
116
- projectName: '',
193
+ projectName,
117
194
  readmeTemplates,
118
195
  },
119
196
  });