@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.
- package/CHANGELOG.md +13 -0
- package/eslint-suppressions.json +0 -5
- package/package.json +3 -3
- package/plugin/buildAlgoliaIndex.ts +12 -43
- package/plugin/components/SDKSelect.astro +3 -6
- package/plugin/helpers/generateDocsRoutes.ts +32 -0
- package/plugin/helpers/multiSpec.ts +8 -0
- package/plugin/index.ts +53 -46
- package/plugin/loadPluginConfig.ts +131 -62
- package/plugin/react/Routing.tsx +2 -4
- package/plugin/routes/Docs.astro +5 -2
- package/plugin/routes/DocsStatic.astro +2 -4
- package/plugin/routes/Overview.astro +21 -7
- package/plugin/routes/markdown.ts +4 -4
- package/plugin/specs/FileCache.ts +99 -0
- package/plugin/specs/fetchSpecSSR.ts +16 -10
- package/plugin/specs/generateSpec.ts +88 -26
- package/plugin/specs/index.ts +88 -195
- package/plugin/specs/inputResolver.ts +146 -0
- package/virtual-module.d.ts +19 -3
- package/plugin/helpers/getDocsLanguages.ts +0 -9
package/plugin/react/Routing.tsx
CHANGED
|
@@ -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 = (
|
|
241
|
+
const languages = getDocsLanguages();
|
|
244
242
|
|
|
245
243
|
return (
|
|
246
244
|
<SDKSelectReactComponent
|
package/plugin/routes/Docs.astro
CHANGED
|
@@ -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
|
|
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 {
|
|
5
|
-
import { getSDKJSONInSSR } from '../specs/fetchSpecSSR';
|
|
4
|
+
import { generateAllDocsRoutes } from '../helpers/generateDocsRoutes';
|
|
6
5
|
|
|
7
6
|
export const getStaticPaths = (async () => {
|
|
8
|
-
const
|
|
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
|
|
11
|
-
|
|
12
|
-
.filter((
|
|
13
|
-
.filter((
|
|
14
|
-
|
|
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 {
|
|
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
|
|
21
|
-
return
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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
|
|
20
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
async function generateSpecFromStrings({
|
|
19
46
|
oasStr,
|
|
20
47
|
configStr,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
let languages = sdkJson.docs?.languages;
|
|
48
77
|
|
|
49
|
-
|
|
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>>;
|