@stainless-api/docs 0.1.0-beta.138 → 0.1.0-beta.139
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 +12 -0
- package/eslint-suppressions.json +0 -25
- package/eslint.config.ts +1 -5
- package/package.json +7 -9
- package/plugin/buildAlgoliaIndex.ts +6 -12
- package/plugin/index.ts +74 -38
- package/plugin/loadPluginConfig.ts +14 -130
- package/plugin/specs/defaultSpecLoader.ts +192 -0
- package/plugin/specs/fetchSpecSSR.ts +1 -1
- package/plugin/specs/utils.ts +86 -0
- package/tsconfig.json +1 -1
- package/plugin/specs/FileCache.ts +0 -99
- package/plugin/specs/generateSpec.ts +0 -109
- package/plugin/specs/index.ts +0 -132
- package/plugin/specs/inputResolver.ts +0 -148
- package/plugin/specs/worker.ts +0 -199
- package/plugin/vendor/preview.worker.docs.js +0 -20618
- package/plugin/vendor/templates/cli.md +0 -1
- package/plugin/vendor/templates/go.md +0 -316
- package/plugin/vendor/templates/java.md +0 -91
- package/plugin/vendor/templates/kotlin.md +0 -91
- package/plugin/vendor/templates/node.md +0 -235
- package/plugin/vendor/templates/python.md +0 -251
- package/plugin/vendor/templates/ruby.md +0 -147
- package/plugin/vendor/templates/terraform.md +0 -60
- package/plugin/vendor/templates/typescript.md +0 -319
- package/scripts/vendor_deps.ts +0 -50
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import type { SpecLoaderFn, SpecLoaderParams } from './utils';
|
|
3
|
+
import Stainless, { APIError } from '@stainless-api/sdk';
|
|
4
|
+
import { DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
5
|
+
import { bold } from '../../shared/terminalUtils';
|
|
6
|
+
import { mkdir, readdir, readFile, rm, writeFile } from 'fs/promises';
|
|
7
|
+
import { generateSpecFromStrings, previewWorkerCode } from '@stainless/sdk-json/spec';
|
|
8
|
+
import { Spec, SpecLanguage } from '@stainless/sdk-json';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
|
|
11
|
+
function resolvePath(inputPath: string) {
|
|
12
|
+
return path.resolve(process.cwd(), inputPath);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getLocalFilePaths() {
|
|
16
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
17
|
+
const oasPath = process.env.OPENAPI_PATH;
|
|
18
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
19
|
+
const configPath = process.env.STAINLESS_CONFIG_PATH;
|
|
20
|
+
|
|
21
|
+
if (!oasPath || !configPath) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
oasPath: resolvePath(oasPath),
|
|
27
|
+
configPath: resolvePath(configPath),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function fetchVersionInfo(project: string, apiKey: string): Promise<Record<DocsLanguage, string>> {
|
|
32
|
+
const data = await fetch(`https://api.stainless.com/api/projects/${project}/package-versions`, {
|
|
33
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const content = await data.text();
|
|
37
|
+
return JSON.parse(content) as Record<DocsLanguage, string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function redactApiKey(apiKey: string) {
|
|
41
|
+
return apiKey
|
|
42
|
+
.split('')
|
|
43
|
+
.map((char, index) => (index < 10 ? char : '*'))
|
|
44
|
+
.join('');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function loadInputs({ apiKey, logger, stainlessProject, branch }: SpecLoaderParams) {
|
|
48
|
+
const localFilePaths = getLocalFilePaths();
|
|
49
|
+
|
|
50
|
+
if (localFilePaths) {
|
|
51
|
+
try {
|
|
52
|
+
const oasStr = await readFile(localFilePaths.oasPath, 'utf8');
|
|
53
|
+
const configStr = await readFile(localFilePaths.configPath, 'utf8');
|
|
54
|
+
return {
|
|
55
|
+
oasStr,
|
|
56
|
+
configStr,
|
|
57
|
+
versionInfo: null,
|
|
58
|
+
};
|
|
59
|
+
} catch (e) {
|
|
60
|
+
logger.error(bold('Failed to load spec inputs from files:'));
|
|
61
|
+
logger.error(e instanceof Error ? e.message : String(e));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!apiKey) {
|
|
67
|
+
logger.error(
|
|
68
|
+
[
|
|
69
|
+
bold(
|
|
70
|
+
'No Stainless credentials found. Please choose one of the following options to authenticate with Stainless:',
|
|
71
|
+
),
|
|
72
|
+
'- Run `stl auth login` to authenticate via the Stainless CLI',
|
|
73
|
+
'- Provide a Stainless API key via the `STAINLESS_API_KEY` environment variable (eg. in a .env file)',
|
|
74
|
+
'- Set the `apiKey` option in the Stainless Docs config',
|
|
75
|
+
].join('\n'),
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const client = new Stainless({ apiKey });
|
|
82
|
+
const configs = await client.projects.configs.retrieve({
|
|
83
|
+
project: stainlessProject,
|
|
84
|
+
branch: branch,
|
|
85
|
+
include: 'openapi',
|
|
86
|
+
});
|
|
87
|
+
const versionInfo = await fetchVersionInfo(stainlessProject, apiKey);
|
|
88
|
+
|
|
89
|
+
const configYML = Object.values(configs)[0] as { content: unknown };
|
|
90
|
+
const oasJson = Object.values(configs)[1] as { content: unknown };
|
|
91
|
+
const oasStr = oasJson['content'];
|
|
92
|
+
const configStr = configYML['content'];
|
|
93
|
+
|
|
94
|
+
if (typeof oasStr !== 'string' || typeof configStr !== 'string') {
|
|
95
|
+
logger.error('Received invalid OAS or config from Stainless');
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
oasStr,
|
|
101
|
+
configStr,
|
|
102
|
+
versionInfo,
|
|
103
|
+
};
|
|
104
|
+
} catch (e) {
|
|
105
|
+
if (e instanceof APIError && e.status >= 400 && e.status < 500) {
|
|
106
|
+
logger.error(`Failed to load requested project slug: "${stainlessProject}"`);
|
|
107
|
+
if (apiKey) {
|
|
108
|
+
logger.error(`API key: "${redactApiKey(apiKey)}"`);
|
|
109
|
+
}
|
|
110
|
+
logger.error(
|
|
111
|
+
`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.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function maybeLoadJSONFile<T>(filePath: string): Promise<T | null> {
|
|
119
|
+
try {
|
|
120
|
+
const fileContents = await readFile(filePath, 'utf8');
|
|
121
|
+
return JSON.parse(fileContents) as T;
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function cleanupDirectory(directory: string, filesToKeep: string[]) {
|
|
128
|
+
const allFiles = await readdir(directory);
|
|
129
|
+
const unusedFiles = allFiles.filter((file) => !filesToKeep.includes(file));
|
|
130
|
+
await Promise.all(unusedFiles.map((file) => rm(path.join(directory, file))));
|
|
131
|
+
return {
|
|
132
|
+
deletedCount: unusedFiles.length,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const defaultSpecLoader: SpecLoaderFn = async (params) => {
|
|
137
|
+
const { createCodegenDir } = params;
|
|
138
|
+
|
|
139
|
+
const inputs = await loadInputs(params);
|
|
140
|
+
|
|
141
|
+
const specsDirectory = path.join(createCodegenDir().pathname, 'specs2');
|
|
142
|
+
await mkdir(specsDirectory, { recursive: true });
|
|
143
|
+
|
|
144
|
+
const fileName =
|
|
145
|
+
crypto
|
|
146
|
+
.createHash('sha256')
|
|
147
|
+
.update(JSON.stringify(inputs) + previewWorkerCode)
|
|
148
|
+
.digest('hex')
|
|
149
|
+
.slice(0, 10) + '.json';
|
|
150
|
+
|
|
151
|
+
const filePath = path.join(specsDirectory, fileName);
|
|
152
|
+
|
|
153
|
+
const cachedSpec = await maybeLoadJSONFile<{ languages: SpecLanguage[]; sdkJson: Spec }>(filePath);
|
|
154
|
+
// skip generation since we already have a cached spec
|
|
155
|
+
if (cachedSpec) {
|
|
156
|
+
params.logger.info(`Loaded cached spec: ${fileName}`);
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
filePath,
|
|
160
|
+
languages: cachedSpec.languages,
|
|
161
|
+
sdkJson: cachedSpec.sdkJson,
|
|
162
|
+
},
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await generateSpecFromStrings({
|
|
167
|
+
oasStr: inputs.oasStr,
|
|
168
|
+
configStr: inputs.configStr,
|
|
169
|
+
languageOverrides: {
|
|
170
|
+
mode: 'exclude',
|
|
171
|
+
list: params.excludeLanguages ?? [],
|
|
172
|
+
},
|
|
173
|
+
versionInfo: inputs.versionInfo,
|
|
174
|
+
stainlessProject: params.stainlessProject,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await writeFile(filePath, JSON.stringify(result), 'utf8');
|
|
178
|
+
params.logger.info(`Generated: ${fileName}`);
|
|
179
|
+
|
|
180
|
+
const { deletedCount } = await cleanupDirectory(specsDirectory, [fileName]);
|
|
181
|
+
if (deletedCount > 0) {
|
|
182
|
+
params.logger.info(`Cleaned up ${deletedCount} unused spec file(s)`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
filePath,
|
|
188
|
+
languages: result.languages.filter((language) => language !== 'sql' && language !== 'openapi'),
|
|
189
|
+
sdkJson: result.sdkJson,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile } from 'fs/promises';
|
|
2
2
|
|
|
3
3
|
import { api } from 'virtual:stainless-apis-manifest';
|
|
4
|
-
import { SpecWithAuth } from '
|
|
4
|
+
import type { SpecWithAuth } from '@stainless/sdk-json/spec';
|
|
5
5
|
import { DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
6
6
|
|
|
7
7
|
const cachedSpecWithAuth: Record<string, SpecWithAuth> = {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Spec } from '@stainless/sdk-json';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
4
|
+
import { AstroIntegrationLogger } from 'astro';
|
|
5
|
+
|
|
6
|
+
type PossibleLanguage = NonNullable<NonNullable<NonNullable<Spec['docs']>['languages']>[number]>;
|
|
7
|
+
|
|
8
|
+
export type SpecLoaderParams = {
|
|
9
|
+
/**
|
|
10
|
+
* The slug of your Stainless project.
|
|
11
|
+
*/
|
|
12
|
+
stainlessProject: string;
|
|
13
|
+
/**
|
|
14
|
+
* The branch of your Stainless project.
|
|
15
|
+
*/
|
|
16
|
+
branch: string;
|
|
17
|
+
/**
|
|
18
|
+
* The Stainless API key. This can be used to make requests against the Stainless API.
|
|
19
|
+
*/
|
|
20
|
+
apiKey: string | null;
|
|
21
|
+
/**
|
|
22
|
+
* The languages that the user has explicitly asked to be excluded from the API reference.
|
|
23
|
+
*/
|
|
24
|
+
excludeLanguages: DocsLanguage[] | null;
|
|
25
|
+
/**
|
|
26
|
+
* An Astro logger. This should be used for logging messages.
|
|
27
|
+
*/
|
|
28
|
+
logger: AstroIntegrationLogger;
|
|
29
|
+
/**
|
|
30
|
+
* A function that creates a directory in .astro. See: https://docs.astro.build/en/reference/integrations-reference/#createcodegendir
|
|
31
|
+
*/
|
|
32
|
+
createCodegenDir: () => URL;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type SpecLoaderResult = {
|
|
36
|
+
/**
|
|
37
|
+
* The file path to the loaded spec. The spec MUST be written to a path on disk.
|
|
38
|
+
* If you are not sure where to place it, create a directory in .astro using the `createCodegenDir` function passed in the parameters of the spec loader function.
|
|
39
|
+
*/
|
|
40
|
+
filePath: string;
|
|
41
|
+
/**
|
|
42
|
+
* The languages that are represented in the spec.
|
|
43
|
+
*/
|
|
44
|
+
languages: PossibleLanguage[];
|
|
45
|
+
/**
|
|
46
|
+
* Optionally, if you already have already the spec in memory, you can provide it. If not provided, the contents of the file at `filePath` will be read.
|
|
47
|
+
* IMPORTANT: This should be equivalent to the contents of the file at `filePath`. If not, bugs and inconsistencies will arise.
|
|
48
|
+
*/
|
|
49
|
+
sdkJson?: Spec;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type SpecLoaderFn = (opts: SpecLoaderParams) => Promise<SpecLoaderResult[]>;
|
|
53
|
+
|
|
54
|
+
async function readSpecFromFile(filePath: string) {
|
|
55
|
+
const txt = await readFile(filePath, 'utf8');
|
|
56
|
+
const json = JSON.parse(txt) as Spec;
|
|
57
|
+
return json;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function loadAllSpecs(specLoaderResultsPromise: Promise<SpecLoaderResult[]>) {
|
|
61
|
+
const specLoaderResults = await specLoaderResultsPromise;
|
|
62
|
+
const specs = await Promise.all(
|
|
63
|
+
specLoaderResults.map(async (result) => {
|
|
64
|
+
return {
|
|
65
|
+
filePath: result.filePath,
|
|
66
|
+
languages: result.languages,
|
|
67
|
+
sdkJson: result.sdkJson ?? (await readSpecFromFile(result.filePath)),
|
|
68
|
+
};
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
return specs;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type LoadedSpecs = Awaited<ReturnType<typeof loadAllSpecs>>;
|
|
75
|
+
|
|
76
|
+
export function flatSpecsList(specs: LoadedSpecs) {
|
|
77
|
+
return specs
|
|
78
|
+
.map((s) =>
|
|
79
|
+
s.languages.map((language) => ({
|
|
80
|
+
language,
|
|
81
|
+
sdkJson: s.sdkJson,
|
|
82
|
+
filePath: s.filePath,
|
|
83
|
+
})),
|
|
84
|
+
)
|
|
85
|
+
.flat();
|
|
86
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,99 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
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?.install && meta?.version) {
|
|
89
|
-
meta.install = meta.install.replace(meta.version, version);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (meta?.version) meta.version = version;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
sdkJson,
|
|
98
|
-
languages,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export const specCache = new FileCache({
|
|
103
|
-
generate: generateSpecFromStrings,
|
|
104
|
-
globalHashBits: previewWorkerCode, // you can change this as a last resort to invalidate the cache
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
export type SpecCacheResult = Awaited<ReturnType<typeof specCache.get>>;
|
|
108
|
-
|
|
109
|
-
export type SpecWithAuth = Awaited<ReturnType<typeof generateSpecFromStrings>>;
|
package/plugin/specs/index.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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>>;
|