@stainless-api/docs 0.1.0-beta.93 → 0.1.0-beta.94

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.
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import { marked, Tokens } from 'marked';
3
+ import { getDocsLanguages } from '../helpers/multiSpec';
3
4
 
4
5
  import {
5
6
  createMarkdownProcessor,
@@ -44,7 +45,6 @@ import { Dropdown } from '@stainless-api/docs/components';
44
45
 
45
46
  import {
46
47
  RESOLVED_API_REFERENCE_PATH,
47
- EXCLUDE_LANGUAGES,
48
48
  EXPAND_RESOURCES,
49
49
  HIGHLIGHT_THEMES,
50
50
  BREADCRUMB_CONFIG,
@@ -237,10 +237,8 @@ export function SDKSelectReactComponent({
237
237
  }
238
238
 
239
239
  function SDKRequestTitle({ snippetLanguage }: SDKRequestTitleProps) {
240
- const spec = useSpec();
241
-
242
240
  const selected = snippetLanguage.split('.').at(0) as DocsLanguage;
243
- const languages = (spec?.docs?.languages ?? ['http']).filter((lang) => !EXCLUDE_LANGUAGES.includes(lang));
241
+ const languages = getDocsLanguages();
244
242
 
245
243
  return (
246
244
  <SDKSelectReactComponent
@@ -2,17 +2,20 @@
2
2
  import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
3
3
  import { getReadmeContent, buildPageNavigation, RenderSpec, astroMarkdownRender } from '../react/Routing';
4
4
  import { getResourceFromSpec } from '@stainless-api/docs-ui/utils';
5
- import { parseStainlessPath } from '@stainless-api/docs-ui/routing';
5
+ import { parseRoute, parseStainlessPath } from '@stainless-api/docs-ui/routing';
6
6
  import {
7
7
  CONTENT_PANEL_LAYOUT,
8
8
  MIDDLEWARE,
9
9
  EXPERIMENTAL_REQUEST_BUILDER,
10
+ RESOLVED_API_REFERENCE_PATH,
10
11
  } from 'virtual:stl-starlight-virtual-module';
11
12
  import { generateDocsRoutes } from '../helpers/generateDocsRoutes';
12
13
  import { StainlessIslands } from '../components/StainlessIslands';
13
14
  import { getSDKJSONInSSR } from '../specs/fetchSpecSSR';
14
15
 
15
- const spec = await getSDKJSONInSSR();
16
+ const language = parseRoute(RESOLVED_API_REFERENCE_PATH, Astro.url.pathname)?.language;
17
+
18
+ const spec = await getSDKJSONInSSR(language);
16
19
 
17
20
  export type Props = Awaited<ReturnType<typeof generateDocsRoutes>>[number]['props'] | Record<string, never>;
18
21
 
@@ -1,12 +1,10 @@
1
1
  ---
2
2
  import type { GetStaticPaths } from 'astro';
3
3
  import Docs from './Docs.astro';
4
- import { generateDocsRoutes } from '../helpers/generateDocsRoutes';
5
- import { getSDKJSONInSSR } from '../specs/fetchSpecSSR';
4
+ import { generateAllDocsRoutes } from '../helpers/generateDocsRoutes';
6
5
 
7
6
  export const getStaticPaths = (async () => {
8
- const spec = await getSDKJSONInSSR();
9
- const routes = generateDocsRoutes(spec);
7
+ const routes = await generateAllDocsRoutes();
10
8
  return routes;
11
9
  }) satisfies GetStaticPaths;
12
10
 
@@ -1,17 +1,31 @@
1
1
  ---
2
2
  import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';
3
3
  import { EXCLUDE_LANGUAGES } from 'virtual:stl-starlight-virtual-module';
4
- import type { DocsLanguage } from '@stainless-api/docs-ui/routing';
5
4
  import { RenderLibraries, RenderSpecOverview, type SpecMetadata } from '../react/Routing';
6
5
  import { getSDKJSONInSSR } from '../specs/fetchSpecSSR';
6
+ import { api } from 'virtual:stainless-apis-manifest';
7
7
 
8
- const spec = await getSDKJSONInSSR();
8
+ const spec = await getSDKJSONInSSR('http');
9
9
 
10
- const languages: DocsLanguage[] = spec.docs!.languages ?? ['http'];
11
- const metadata: SpecMetadata = languages
12
- .filter((language) => !['http', 'terraform'].includes(language) && spec.metadata[language])
13
- .filter((language) => !EXCLUDE_LANGUAGES.includes(language))
14
- .map<SpecMetadata[number]>((language) => [language, spec.metadata[language]!]);
10
+ const langTargets = api.languages
11
+ .map((l) => l.language)
12
+ .filter((l) => !EXCLUDE_LANGUAGES.includes(l))
13
+ .filter((l) => l !== 'http' && l !== 'terraform');
14
+
15
+ const langsWithSpecs = await Promise.all(
16
+ langTargets.map(async (language) => {
17
+ const spec = await getSDKJSONInSSR(language);
18
+ return {
19
+ language,
20
+ spec,
21
+ };
22
+ }),
23
+ );
24
+
25
+ const metadata: SpecMetadata = langsWithSpecs.map(({ language, spec }) => [
26
+ language,
27
+ spec.metadata[language]!,
28
+ ]);
15
29
 
16
30
  // PageTitle override will skip rendering the default Starlight title
17
31
  Astro.locals._stlStarlightPage = {
@@ -7,7 +7,7 @@ import { renderMarkdown } from '@stainless-api/docs-ui/markdown';
7
7
  import { parseStainlessPath, type DocsLanguage } from '@stainless-api/docs-ui/routing';
8
8
  import type { EnvironmentType } from '@stainless-api/docs-ui/markdown/utils';
9
9
  import { PROPERTY_SETTINGS, MIDDLEWARE } from 'virtual:stl-starlight-virtual-module';
10
- import { generateDocsRoutes } from '../helpers/generateDocsRoutes';
10
+ import { generateAllDocsRoutes } from '../helpers/generateDocsRoutes';
11
11
  import { getSDKJSONInSSR } from '../specs/fetchSpecSSR';
12
12
 
13
13
  type RouteProps = {
@@ -17,12 +17,12 @@ type RouteProps = {
17
17
  };
18
18
 
19
19
  export const getStaticPaths = (async () => {
20
- const spec = await getSDKJSONInSSR();
21
- return generateDocsRoutes(spec);
20
+ const paths = await generateAllDocsRoutes();
21
+ return paths;
22
22
  }) satisfies GetStaticPaths;
23
23
 
24
24
  export const GET: APIRoute<RouteProps> = async ({ props }) => {
25
- const spec = await getSDKJSONInSSR();
25
+ const spec = await getSDKJSONInSSR(props.language);
26
26
 
27
27
  if (props.kind === 'readme') {
28
28
  const readmeContent = await getReadmeContent(spec, props.language);
@@ -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
+ }
@@ -1,21 +1,27 @@
1
1
  import { readFile } from 'fs/promises';
2
2
 
3
- import { specPath } from 'virtual:stainless-sdk-json-manifest';
3
+ import { api } from 'virtual:stainless-apis-manifest';
4
4
  import { SpecWithAuth } from './generateSpec';
5
+ import { DocsLanguage } from '@stainless-api/docs-ui/routing';
5
6
 
6
- let cachedSpecWithAuth: SpecWithAuth | null = null;
7
+ const cachedSpecWithAuth: Record<string, SpecWithAuth> = {};
7
8
 
8
- async function getSpecWithAuthInSSR() {
9
- if (cachedSpecWithAuth) {
10
- return cachedSpecWithAuth;
9
+ async function getSpecWithAuthInSSR(filePath: string) {
10
+ if (cachedSpecWithAuth[filePath]) {
11
+ return cachedSpecWithAuth[filePath];
11
12
  }
12
- const specStr = await readFile(specPath, 'utf8');
13
+ const specStr = await readFile(filePath, 'utf8');
13
14
  const json = JSON.parse(specStr) as SpecWithAuth;
14
- cachedSpecWithAuth = json;
15
+ cachedSpecWithAuth[filePath] = json;
15
16
  return json;
16
17
  }
17
18
 
18
- export async function getSDKJSONInSSR() {
19
- const specWithAuth = await getSpecWithAuthInSSR();
20
- return specWithAuth.data;
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;
21
27
  }
@@ -1,29 +1,54 @@
1
1
  import type * as SDKJSON from '@stainless/sdk-json';
2
2
  import { Languages } from '@stainless-api/docs-ui/routing';
3
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';
4
7
 
5
- function addAuthToSpec(spec: SDKJSON.Spec, config: ParsedConfig) {
6
- const opts = Object.entries(config.client_settings.opts).map(([k, v]) => ({ name: k, ...v }));
7
- return {
8
- data: spec,
9
- auth: spec.security_schemes.map((scheme) => ({
10
- ...scheme,
11
- opts: opts.filter((opt) => opt.auth?.security_scheme === scheme.name),
12
- })),
13
- };
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[];
14
22
  }
15
23
 
16
- export type SpecWithAuth = ReturnType<typeof addAuthToSpec>;
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
+ }
17
44
 
18
- export async function generateSpecFromStrings({
45
+ async function generateSpecFromStrings({
19
46
  oasStr,
20
47
  configStr,
21
- projectName,
22
- }: {
23
- oasStr: string;
24
- configStr: string;
25
- projectName: string;
26
- }) {
48
+ stainlessProject,
49
+ languageOverrides,
50
+ versionInfo,
51
+ }: GenerateSpecRawInputs) {
27
52
  const { oas, config } = await parseInputs({
28
53
  oas: oasStr,
29
54
  config: configStr,
@@ -31,20 +56,57 @@ export async function generateSpecFromStrings({
31
56
 
32
57
  const transformedOAS = await transformOAS({ oas, config });
33
58
 
34
- const languages =
35
- config.docs?.languages ??
36
- (Object.entries(config.targets)
37
- .filter(([name, target]) => Languages.includes(name) && !target.skip)
38
- .map(([name]) => name) as SDKJSON.SpecLanguage[]);
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);
39
66
 
67
+ // SDKJSON has weird behavior where it will create a spec with HTTP, even if it's not in the languages list
40
68
  const sdkJson = await createSDKJSON({
41
69
  oas: transformedOAS,
42
70
  config,
43
- languages,
44
- projectName,
71
+ // if language overrides are provided, use them, otherwise use the languages from the Stainless config
72
+ languages: languagesToGenerate,
73
+ projectName: stainlessProject,
45
74
  });
46
75
 
47
- const specWithAuth = addAuthToSpec(sdkJson, config);
76
+ let languages = sdkJson.docs?.languages;
48
77
 
49
- return specWithAuth;
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
+ };
50
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>>;