@stainless-api/docs 0.1.0-beta.82 → 0.1.0-beta.83
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 +1 -1
- package/package.json +4 -4
- package/plugin/buildAlgoliaIndex.ts +1 -1
- package/plugin/cms/generate-spec.ts +112 -0
- package/plugin/cms/server.ts +84 -59
- package/plugin/cms/sidebar-builder.ts +14 -11
- package/plugin/cms/worker.ts +3 -4
- package/plugin/index.ts +29 -17
- package/plugin/routes/Docs.astro +14 -12
- package/plugin/routes/DocsStatic.astro +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @stainless-api/docs
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.83
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 96e974e: clean up n^2 calls to generateDocsRoutes by passing props
|
|
8
|
+
- a0301a2: Improves build speed by up to 8x for large APIs
|
|
9
|
+
- b6b9d30: fix: should hide skipped resources in sidebar
|
|
10
|
+
- Updated dependencies [3037a19]
|
|
11
|
+
- @stainless-api/ui-primitives@0.1.0-beta.45
|
|
12
|
+
- @stainless-api/docs-search@0.1.0-beta.14
|
|
13
|
+
- @stainless-api/docs-ui@0.1.0-beta.61
|
|
14
|
+
|
|
3
15
|
## 0.1.0-beta.82
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/eslint-suppressions.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stainless-api/docs",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.83",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"vite-plugin-prebundle-workers": "^0.2.0",
|
|
59
59
|
"web-worker": "^1.5.0",
|
|
60
60
|
"yaml": "^2.8.2",
|
|
61
|
-
"@stainless-api/docs-search": "0.1.0-beta.
|
|
62
|
-
"@stainless-api/docs-ui": "0.1.0-beta.
|
|
63
|
-
"@stainless-api/ui-primitives": "0.1.0-beta.
|
|
61
|
+
"@stainless-api/docs-search": "0.1.0-beta.14",
|
|
62
|
+
"@stainless-api/docs-ui": "0.1.0-beta.61",
|
|
63
|
+
"@stainless-api/ui-primitives": "0.1.0-beta.45"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@astrojs/check": "^0.9.6",
|
|
@@ -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 path from 'path';
|
|
5
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
|
|
8
|
+
function addAuthToSpec(spec: SDKJSON.Spec, config: ParsedConfig) {
|
|
9
|
+
const opts = Object.entries(config.client_settings.opts).map(([k, v]) => ({ name: k, ...v }));
|
|
10
|
+
return {
|
|
11
|
+
data: spec,
|
|
12
|
+
auth: spec.security_schemes.map((scheme) => ({
|
|
13
|
+
...scheme,
|
|
14
|
+
opts: opts.filter((opt) => opt.auth?.security_scheme === scheme.name),
|
|
15
|
+
})),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SpecWithAuth = ReturnType<typeof addAuthToSpec>;
|
|
20
|
+
|
|
21
|
+
function hashStringBase64(input: string, algo = 'sha256') {
|
|
22
|
+
return createHash(algo).update(input, 'utf8').digest('base64');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class SpecFileSystemCache {
|
|
26
|
+
private specFilePath: string;
|
|
27
|
+
private shaForInputsPath: string;
|
|
28
|
+
|
|
29
|
+
private hashInputs() {
|
|
30
|
+
return hashStringBase64(this.currentInputs.oas + this.currentInputs.config);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async write(spec: SpecWithAuth) {
|
|
34
|
+
await writeFile(this.specFilePath, JSON.stringify(spec), 'utf-8');
|
|
35
|
+
await writeFile(this.shaForInputsPath, this.hashInputs(), 'utf-8');
|
|
36
|
+
console.log('wrote spec to cache', this.specFilePath);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async read() {
|
|
40
|
+
try {
|
|
41
|
+
const cachedSha = await readFile(this.shaForInputsPath, 'utf-8');
|
|
42
|
+
if (cachedSha !== this.hashInputs()) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const cachedSpec = JSON.parse(await readFile(this.specFilePath, 'utf-8')) as SpecWithAuth;
|
|
51
|
+
console.log('read spec from cache', this.specFilePath);
|
|
52
|
+
return cachedSpec;
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
constructor(
|
|
59
|
+
cacheDirectory: string,
|
|
60
|
+
private currentInputs: { oas: string; config: string },
|
|
61
|
+
) {
|
|
62
|
+
this.specFilePath = path.join(cacheDirectory, 'spec.json');
|
|
63
|
+
this.shaForInputsPath = path.join(cacheDirectory, 'spec_inputs_sha.txt');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function generateSpecFromStrings({
|
|
68
|
+
oasStr,
|
|
69
|
+
configStr,
|
|
70
|
+
specCacheDirectory,
|
|
71
|
+
projectName,
|
|
72
|
+
}: {
|
|
73
|
+
oasStr: string;
|
|
74
|
+
configStr: string;
|
|
75
|
+
specCacheDirectory: string | null;
|
|
76
|
+
projectName: string;
|
|
77
|
+
}) {
|
|
78
|
+
const fsCache = specCacheDirectory
|
|
79
|
+
? new SpecFileSystemCache(specCacheDirectory, { oas: oasStr, config: configStr })
|
|
80
|
+
: null;
|
|
81
|
+
|
|
82
|
+
const cachedSpec = await fsCache?.read();
|
|
83
|
+
if (cachedSpec) {
|
|
84
|
+
return cachedSpec;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { oas, config } = await parseInputs({
|
|
88
|
+
oas: oasStr,
|
|
89
|
+
config: configStr,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const transformedOAS = await transformOAS({ oas, config });
|
|
93
|
+
|
|
94
|
+
const languages =
|
|
95
|
+
config.docs?.languages ??
|
|
96
|
+
(Object.entries(config.targets)
|
|
97
|
+
.filter(([name, target]) => Languages.includes(name) && !target.skip)
|
|
98
|
+
.map(([name]) => name) as SDKJSON.SpecLanguage[]);
|
|
99
|
+
|
|
100
|
+
const sdkJson = await createSDKJSON({
|
|
101
|
+
oas: transformedOAS,
|
|
102
|
+
config,
|
|
103
|
+
languages,
|
|
104
|
+
projectName,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const specWithAuth = addAuthToSpec(sdkJson, config);
|
|
108
|
+
|
|
109
|
+
await fsCache?.write(specWithAuth);
|
|
110
|
+
|
|
111
|
+
return specWithAuth;
|
|
112
|
+
}
|
package/plugin/cms/server.ts
CHANGED
|
@@ -3,8 +3,7 @@ import { createServer, IncomingMessage } from 'http';
|
|
|
3
3
|
import { readFile } from 'fs/promises';
|
|
4
4
|
|
|
5
5
|
import type * as SDKJSON from '@stainless/sdk-json';
|
|
6
|
-
import {
|
|
7
|
-
import { Languages, parseRoute, type DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
6
|
+
import { parseRoute, type DocsLanguage } from '@stainless-api/docs-ui/routing';
|
|
8
7
|
import Stainless, { APIError } from '@stainless-api/sdk';
|
|
9
8
|
|
|
10
9
|
import {
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
} from './sidebar-builder';
|
|
15
14
|
import type { VersionUserConfig } from '../loadPluginConfig';
|
|
16
15
|
import { bold } from '../../shared/terminalUtils';
|
|
16
|
+
import { generateSpecFromStrings } from './generate-spec';
|
|
17
17
|
export type InputFilePaths = {
|
|
18
18
|
oasPath?: string;
|
|
19
19
|
configPath?: string;
|
|
@@ -59,18 +59,22 @@ export type Auth = Array<{
|
|
|
59
59
|
}[];
|
|
60
60
|
}>;
|
|
61
61
|
|
|
62
|
+
type LoadedSpec = { data: SDKJSON.Spec; auth: Auth; id: string };
|
|
63
|
+
|
|
62
64
|
async function loadSpec({
|
|
63
65
|
apiKey,
|
|
64
66
|
devPaths,
|
|
65
67
|
version,
|
|
66
68
|
logger,
|
|
69
|
+
specCacheDirectory,
|
|
67
70
|
}: {
|
|
68
71
|
apiKey: string;
|
|
69
72
|
devPaths: InputFilePaths;
|
|
70
73
|
version: VersionUserConfig;
|
|
71
74
|
logger: AstroIntegrationLogger;
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
specCacheDirectory: string | null;
|
|
76
|
+
}): Promise<LoadedSpec> {
|
|
77
|
+
async function unsafeLoad(): Promise<LoadedSpec> {
|
|
74
78
|
let oasStr: string;
|
|
75
79
|
let configStr: string;
|
|
76
80
|
let versions: Record<DocsLanguage, string> | undefined;
|
|
@@ -96,42 +100,25 @@ async function loadSpec({
|
|
|
96
100
|
configStr = configYML['content'];
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const transformedOAS = await transformOAS({ oas, config });
|
|
105
|
-
|
|
106
|
-
const languages =
|
|
107
|
-
config.docs?.languages ??
|
|
108
|
-
(Object.entries(config.targets)
|
|
109
|
-
.filter(([name, target]) => Languages.includes(name) && !target.skip)
|
|
110
|
-
.map(([name]) => name) as SDKJSON.SpecLanguage[]);
|
|
111
|
-
|
|
112
|
-
const sdkJson = await createSDKJSON({
|
|
113
|
-
oas: transformedOAS,
|
|
114
|
-
config,
|
|
115
|
-
languages,
|
|
116
|
-
version,
|
|
103
|
+
const sdkJson = await generateSpecFromStrings({
|
|
104
|
+
oasStr,
|
|
105
|
+
configStr,
|
|
106
|
+
projectName: version.stainlessProject,
|
|
107
|
+
specCacheDirectory,
|
|
117
108
|
});
|
|
118
109
|
|
|
119
110
|
if (versions) {
|
|
120
111
|
for (const [lang, version] of Object.entries(versions)) {
|
|
121
|
-
const meta = sdkJson.metadata[lang as DocsLanguage];
|
|
112
|
+
const meta = sdkJson.data.metadata[lang as DocsLanguage];
|
|
122
113
|
if (meta?.version) meta.version = version;
|
|
123
114
|
}
|
|
124
115
|
}
|
|
125
116
|
|
|
126
117
|
const id = crypto.randomUUID();
|
|
127
|
-
const opts = Object.entries(config.client_settings.opts).map(([k, v]) => ({ name: k, ...v }));
|
|
128
118
|
|
|
129
119
|
return {
|
|
130
|
-
data: sdkJson,
|
|
131
|
-
auth: sdkJson.
|
|
132
|
-
...scheme,
|
|
133
|
-
opts: opts.filter((opt) => opt.auth?.security_scheme === scheme.name),
|
|
134
|
-
})),
|
|
120
|
+
data: sdkJson.data,
|
|
121
|
+
auth: sdkJson.auth,
|
|
135
122
|
id,
|
|
136
123
|
};
|
|
137
124
|
}
|
|
@@ -163,6 +150,7 @@ class Spec {
|
|
|
163
150
|
|
|
164
151
|
reload() {
|
|
165
152
|
this.specPromise = loadSpec({
|
|
153
|
+
specCacheDirectory: this.specCacheDirectory,
|
|
166
154
|
apiKey: this.apiKey,
|
|
167
155
|
devPaths: this.devPaths,
|
|
168
156
|
version: this.version,
|
|
@@ -194,8 +182,10 @@ class Spec {
|
|
|
194
182
|
version: VersionUserConfig,
|
|
195
183
|
devPaths: InputFilePaths,
|
|
196
184
|
private logger: AstroIntegrationLogger,
|
|
185
|
+
private specCacheDirectory: string | null,
|
|
197
186
|
) {
|
|
198
187
|
this.specPromise = loadSpec({
|
|
188
|
+
specCacheDirectory,
|
|
199
189
|
apiKey,
|
|
200
190
|
devPaths,
|
|
201
191
|
version,
|
|
@@ -211,7 +201,7 @@ class Spec {
|
|
|
211
201
|
export type SpecResp = Awaited<ReturnType<Spec['getSpec']>>;
|
|
212
202
|
|
|
213
203
|
// will be necessary once we add POST methods
|
|
214
|
-
function readJsonBody(req: IncomingMessage): Promise<
|
|
204
|
+
function readJsonBody<T>(req: IncomingMessage): Promise<T> {
|
|
215
205
|
return new Promise((resolve, reject) => {
|
|
216
206
|
let body = '';
|
|
217
207
|
req.on('data', (chunk) => {
|
|
@@ -220,7 +210,7 @@ function readJsonBody(req: IncomingMessage): Promise<any> {
|
|
|
220
210
|
req.on('end', () => {
|
|
221
211
|
try {
|
|
222
212
|
const data = JSON.parse(body);
|
|
223
|
-
resolve(data);
|
|
213
|
+
resolve(data as T);
|
|
224
214
|
} catch (error) {
|
|
225
215
|
reject(error);
|
|
226
216
|
}
|
|
@@ -228,6 +218,28 @@ function readJsonBody(req: IncomingMessage): Promise<any> {
|
|
|
228
218
|
});
|
|
229
219
|
}
|
|
230
220
|
|
|
221
|
+
function markCurrentItems(sidebar: SidebarEntry[], currentSlug: string) {
|
|
222
|
+
// IMPORTANT: we need to clone the sidebar to avoid mutating the original sidebar
|
|
223
|
+
const mutableSidebarInstance = structuredClone(sidebar);
|
|
224
|
+
|
|
225
|
+
function recursiveMarkCurrent(entries: SidebarEntry[]) {
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (entry.type === 'link') {
|
|
228
|
+
entry.isCurrent = entry.href === currentSlug;
|
|
229
|
+
if (entry.isCurrent) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (entry.type === 'group') {
|
|
234
|
+
recursiveMarkCurrent(entry.entries);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
recursiveMarkCurrent(mutableSidebarInstance);
|
|
239
|
+
|
|
240
|
+
return mutableSidebarInstance;
|
|
241
|
+
}
|
|
242
|
+
|
|
231
243
|
export function startDevServer({
|
|
232
244
|
port,
|
|
233
245
|
version,
|
|
@@ -235,15 +247,19 @@ export function startDevServer({
|
|
|
235
247
|
apiKey,
|
|
236
248
|
getGeneratedSidebarConfig,
|
|
237
249
|
logger,
|
|
250
|
+
specCacheDirectory,
|
|
238
251
|
}: {
|
|
239
252
|
port: number;
|
|
240
253
|
version: VersionUserConfig;
|
|
241
254
|
devPaths: InputFilePaths;
|
|
242
255
|
apiKey: string;
|
|
256
|
+
specCacheDirectory: string | null;
|
|
243
257
|
getGeneratedSidebarConfig: (id: number) => GeneratedSidebarConfig | null;
|
|
244
258
|
logger: AstroIntegrationLogger;
|
|
245
259
|
}) {
|
|
246
|
-
const spec = new Spec(apiKey, version, devPaths, logger);
|
|
260
|
+
const spec = new Spec(apiKey, version, devPaths, logger, specCacheDirectory);
|
|
261
|
+
|
|
262
|
+
const sidebarCache = new Map<string, SidebarEntry[]>();
|
|
247
263
|
|
|
248
264
|
const server = createServer(async (req, res) => {
|
|
249
265
|
// Add CORS headers
|
|
@@ -266,7 +282,7 @@ export function startDevServer({
|
|
|
266
282
|
|
|
267
283
|
try {
|
|
268
284
|
if (req.method === 'POST' && req.url === '/retrieve_spec') {
|
|
269
|
-
const body = await readJsonBody(req);
|
|
285
|
+
const body = await readJsonBody<{ currentId: string }>(req);
|
|
270
286
|
|
|
271
287
|
const currentSpec = await spec.getSpec(body.currentId);
|
|
272
288
|
|
|
@@ -275,41 +291,49 @@ export function startDevServer({
|
|
|
275
291
|
|
|
276
292
|
if (req.method === 'POST' && req.url === '/build_sidebar') {
|
|
277
293
|
const currentSpec = await spec.forceGetSpec();
|
|
278
|
-
const body = await readJsonBody(req);
|
|
294
|
+
const body = await readJsonBody<{ sidebarId: number; basePath: string; currentSlug: string }>(req);
|
|
279
295
|
const sidebarId: number = body.sidebarId;
|
|
280
296
|
|
|
281
297
|
const sidebarConfig = getGeneratedSidebarConfig(sidebarId);
|
|
282
298
|
|
|
283
299
|
const sidebarGenerateOptions = sidebarConfig?.options;
|
|
284
300
|
|
|
285
|
-
const {
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
+
const { language } = parseRoute(body.basePath, body.currentSlug);
|
|
302
|
+
|
|
303
|
+
const sidebarCacheKey = `${body.basePath}-${language}-${sidebarId}`;
|
|
304
|
+
|
|
305
|
+
const cachedSidebar = sidebarCache.get(sidebarCacheKey);
|
|
306
|
+
let starlightSidebar: SidebarEntry[] | null = null;
|
|
307
|
+
if (cachedSidebar) {
|
|
308
|
+
starlightSidebar = cachedSidebar;
|
|
309
|
+
} else {
|
|
310
|
+
const configItemsBuilder = new SidebarConfigItemsBuilder(
|
|
311
|
+
currentSpec.data,
|
|
312
|
+
language,
|
|
313
|
+
sidebarGenerateOptions,
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
let userSidebarConfig = configItemsBuilder.generateItems();
|
|
317
|
+
|
|
318
|
+
if (sidebarConfig && sidebarConfig.transformFn) {
|
|
319
|
+
const transformedSidebarConfig = sidebarConfig.transformFn(userSidebarConfig, language);
|
|
320
|
+
// the user may not have returned, but they may have mutated the config in place
|
|
321
|
+
// if they did return, we use that
|
|
322
|
+
if (transformedSidebarConfig) {
|
|
323
|
+
userSidebarConfig = transformedSidebarConfig;
|
|
324
|
+
}
|
|
301
325
|
}
|
|
326
|
+
starlightSidebar = toStarlightSidebar({
|
|
327
|
+
basePath: body.basePath,
|
|
328
|
+
spec: currentSpec.data,
|
|
329
|
+
entries: userSidebarConfig,
|
|
330
|
+
currentLanguage: language,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
sidebarCache.set(sidebarCacheKey, starlightSidebar);
|
|
302
334
|
}
|
|
303
|
-
const starlightSidebar = toStarlightSidebar({
|
|
304
|
-
basePath: body.basePath,
|
|
305
|
-
currentSlug: body.currentSlug,
|
|
306
|
-
spec: currentSpec.data,
|
|
307
|
-
entries: userSidebarConfig,
|
|
308
|
-
currentStainlessPath,
|
|
309
|
-
currentLanguage: language,
|
|
310
|
-
});
|
|
311
335
|
|
|
312
|
-
return respond(200, { data: starlightSidebar });
|
|
336
|
+
return respond(200, { data: markCurrentItems(starlightSidebar, body.currentSlug) });
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
return respond(404, { message: 'Not found' });
|
|
@@ -325,6 +349,7 @@ export function startDevServer({
|
|
|
325
349
|
return {
|
|
326
350
|
invalidate: () => {
|
|
327
351
|
spec.reload();
|
|
352
|
+
sidebarCache.clear();
|
|
328
353
|
},
|
|
329
354
|
destroy: function destroy() {
|
|
330
355
|
return new Promise<void>((resolve) => {
|
|
@@ -273,6 +273,7 @@ type SidebarEntry = StarlightRouteData['sidebar'][number];
|
|
|
273
273
|
|
|
274
274
|
// This allows us to be a bit more forgiving to the user.
|
|
275
275
|
// As long as they don't modify the key, we can still find the item.
|
|
276
|
+
|
|
276
277
|
function getResourceOrMethod(spec: SDKJSON.Spec, endpointOrConfigRef: string) {
|
|
277
278
|
for (const entry of walkTree(spec, false)) {
|
|
278
279
|
if (entry.data.kind === 'resource' && entry.data.configRef === endpointOrConfigRef) {
|
|
@@ -306,19 +307,17 @@ export type BuildSidebarParams = {
|
|
|
306
307
|
currentSlug: string;
|
|
307
308
|
};
|
|
308
309
|
|
|
309
|
-
type ToStarlightSidebarParams =
|
|
310
|
+
type ToStarlightSidebarParams = {
|
|
311
|
+
basePath: string;
|
|
310
312
|
spec: SDKJSON.Spec;
|
|
311
313
|
entries: ReferenceSidebarConfigItem[];
|
|
312
|
-
currentStainlessPath: string;
|
|
313
314
|
currentLanguage: DocsLanguage;
|
|
314
315
|
};
|
|
315
316
|
|
|
316
317
|
export function toStarlightSidebar({
|
|
317
318
|
basePath,
|
|
318
|
-
currentSlug,
|
|
319
319
|
spec,
|
|
320
320
|
entries,
|
|
321
|
-
currentStainlessPath,
|
|
322
321
|
currentLanguage,
|
|
323
322
|
}: ToStarlightSidebarParams): SidebarEntry[] {
|
|
324
323
|
const starlightEntries: SidebarEntry[] = [];
|
|
@@ -331,7 +330,7 @@ export function toStarlightSidebar({
|
|
|
331
330
|
type: 'link',
|
|
332
331
|
href: readmeSlug,
|
|
333
332
|
label: entry.label,
|
|
334
|
-
isCurrent:
|
|
333
|
+
isCurrent: false,
|
|
335
334
|
badge: entry.badge,
|
|
336
335
|
attrs: {
|
|
337
336
|
'data-stldocs-overview': 'readme',
|
|
@@ -350,14 +349,12 @@ export function toStarlightSidebar({
|
|
|
350
349
|
language: currentLanguage,
|
|
351
350
|
});
|
|
352
351
|
|
|
353
|
-
const isCurrent = resourceOrMethod.data.stainlessPath === currentStainlessPath;
|
|
354
|
-
|
|
355
352
|
if (resourceOrMethod.data.kind === 'http_method') {
|
|
356
353
|
starlightEntries.push({
|
|
357
354
|
type: 'link',
|
|
358
355
|
href: route,
|
|
359
356
|
label: entry.label,
|
|
360
|
-
isCurrent,
|
|
357
|
+
isCurrent: false,
|
|
361
358
|
badge: entry.badge,
|
|
362
359
|
attrs: {
|
|
363
360
|
'data-stldocs-method': resourceOrMethod.data.httpMethod,
|
|
@@ -368,7 +365,7 @@ export function toStarlightSidebar({
|
|
|
368
365
|
type: 'link',
|
|
369
366
|
href: route,
|
|
370
367
|
label: entry.label,
|
|
371
|
-
isCurrent,
|
|
368
|
+
isCurrent: false,
|
|
372
369
|
badge: entry.badge,
|
|
373
370
|
attrs: {
|
|
374
371
|
// TODO: @Ryan: is data.name unique? This is what we used before so I'm not changing, but I am curious.
|
|
@@ -379,14 +376,20 @@ export function toStarlightSidebar({
|
|
|
379
376
|
throw new Error(`Unknown entry kind ${JSON.stringify(entry)}`);
|
|
380
377
|
}
|
|
381
378
|
} else if (entry.kind === 'group') {
|
|
379
|
+
// Skip pushing the group if if the resource it represents is not available in the current language.
|
|
380
|
+
// This occurs when SDK generation for the current language is skipped in the Stainless config for that resource.
|
|
381
|
+
if (entry.resourceGroupKey) {
|
|
382
|
+
const resourceOrMethod = getResourceOrMethod(spec, entry.resourceGroupKey);
|
|
383
|
+
if (resourceOrMethod?.data?.kind === 'resource' && !resourceOrMethod?.data?.[currentLanguage]) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
382
387
|
starlightEntries.push({
|
|
383
388
|
type: 'group',
|
|
384
389
|
label: entry.label,
|
|
385
390
|
entries: toStarlightSidebar({
|
|
386
391
|
basePath,
|
|
387
|
-
currentSlug,
|
|
388
392
|
spec,
|
|
389
|
-
currentStainlessPath,
|
|
390
393
|
entries: entry.entries,
|
|
391
394
|
currentLanguage,
|
|
392
395
|
}),
|
package/plugin/cms/worker.ts
CHANGED
|
@@ -5,7 +5,6 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { dirname, resolve } from 'node:path';
|
|
6
6
|
import fs from 'fs/promises';
|
|
7
7
|
import pathutils from 'path';
|
|
8
|
-
import type { VersionUserConfig } from '../loadPluginConfig';
|
|
9
8
|
|
|
10
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
10
|
const __dirname = dirname(__filename);
|
|
@@ -161,12 +160,12 @@ export async function createSDKJSON({
|
|
|
161
160
|
oas,
|
|
162
161
|
config,
|
|
163
162
|
languages,
|
|
164
|
-
|
|
163
|
+
projectName,
|
|
165
164
|
}: {
|
|
166
165
|
oas: OpenAPIDocument;
|
|
167
166
|
config: ParsedConfig;
|
|
168
167
|
languages: DocsLanguage[];
|
|
169
|
-
|
|
168
|
+
projectName: string;
|
|
170
169
|
}) {
|
|
171
170
|
const templatePath = resolve(__dirname, '../vendor/templates');
|
|
172
171
|
const readmeLoader = await Promise.all(
|
|
@@ -191,7 +190,7 @@ export async function createSDKJSON({
|
|
|
191
190
|
config,
|
|
192
191
|
languages,
|
|
193
192
|
transform: false,
|
|
194
|
-
projectName
|
|
193
|
+
projectName,
|
|
195
194
|
readmeTemplates,
|
|
196
195
|
},
|
|
197
196
|
});
|
package/plugin/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { AstroIntegration, AstroIntegrationLogger } from 'astro';
|
|
|
4
4
|
import type { BundledTheme } from 'shiki';
|
|
5
5
|
import { config } from 'dotenv';
|
|
6
6
|
import getPort from 'get-port';
|
|
7
|
-
import { startDevServer, type SpecResp } from './cms/server';
|
|
7
|
+
import { DevSpecServer, startDevServer, type SpecResp } from './cms/server';
|
|
8
8
|
import { buildAlgoliaIndex } from './buildAlgoliaIndex';
|
|
9
9
|
import {
|
|
10
10
|
getAPIReferencePlaceholderItemFromSidebarConfig,
|
|
@@ -130,20 +130,7 @@ async function stlStarlightAstroIntegration(
|
|
|
130
130
|
|
|
131
131
|
const { apiKey, version, devPaths } = tmpGetCMSServerConfig(pluginConfig.specRetrieverConfig);
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
port: CMS_PORT,
|
|
135
|
-
apiKey: apiKey.value,
|
|
136
|
-
version,
|
|
137
|
-
devPaths,
|
|
138
|
-
logger: stlStarlightPluginLogger,
|
|
139
|
-
getGeneratedSidebarConfig: (id: number) => {
|
|
140
|
-
const config = sidebarConfigs.get(id);
|
|
141
|
-
if (!config) {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
return config;
|
|
145
|
-
},
|
|
146
|
-
});
|
|
133
|
+
let cmsServer: DevSpecServer | undefined;
|
|
147
134
|
|
|
148
135
|
let building: Promise<void> | undefined;
|
|
149
136
|
let reportError: ((message: string) => void) | null = null;
|
|
@@ -186,11 +173,36 @@ async function stlStarlightAstroIntegration(
|
|
|
186
173
|
logger: localLogger,
|
|
187
174
|
command,
|
|
188
175
|
config: astroConfig,
|
|
176
|
+
createCodegenDir,
|
|
189
177
|
}) => {
|
|
190
178
|
const logger = getSharedLogger({ fallback: localLogger });
|
|
191
179
|
const projectDir = astroConfig.root.pathname;
|
|
192
180
|
astroBase = astroConfig.base;
|
|
193
181
|
|
|
182
|
+
const codegenDir = createCodegenDir().pathname;
|
|
183
|
+
|
|
184
|
+
let specCacheDirectory: string | null = null;
|
|
185
|
+
if (command === 'dev') {
|
|
186
|
+
specCacheDirectory = path.join(codegenDir, 'spec_cache');
|
|
187
|
+
fs.mkdirSync(specCacheDirectory, { recursive: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
cmsServer = startDevServer({
|
|
191
|
+
port: CMS_PORT,
|
|
192
|
+
apiKey: apiKey.value,
|
|
193
|
+
version,
|
|
194
|
+
devPaths,
|
|
195
|
+
logger: stlStarlightPluginLogger,
|
|
196
|
+
specCacheDirectory,
|
|
197
|
+
getGeneratedSidebarConfig: (id: number) => {
|
|
198
|
+
const config = sidebarConfigs.get(id);
|
|
199
|
+
if (!config) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
return config;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
194
206
|
reportError = (message: string) => logger.error(message);
|
|
195
207
|
|
|
196
208
|
if (pluginConfig.experimentalPlaygrounds) {
|
|
@@ -248,7 +260,7 @@ async function stlStarlightAstroIntegration(
|
|
|
248
260
|
server.watcher.on('change', async (changed) => {
|
|
249
261
|
if (Object.values(devPaths).includes(changed)) {
|
|
250
262
|
logger.info(`${changed} changed, reloading...`);
|
|
251
|
-
cmsServer
|
|
263
|
+
cmsServer?.invalidate();
|
|
252
264
|
server.hot.send({
|
|
253
265
|
type: 'full-reload',
|
|
254
266
|
path: '*',
|
|
@@ -341,7 +353,7 @@ async function stlStarlightAstroIntegration(
|
|
|
341
353
|
});
|
|
342
354
|
},
|
|
343
355
|
'astro:server:done': async () => {
|
|
344
|
-
await cmsServer
|
|
356
|
+
await cmsServer?.destroy();
|
|
345
357
|
},
|
|
346
358
|
'astro:build:start'({ logger }) {
|
|
347
359
|
collectedErrors = [];
|
package/plugin/routes/Docs.astro
CHANGED
|
@@ -13,32 +13,34 @@ import { generateDocsRoutes } from '../helpers/generateDocsRoutes';
|
|
|
13
13
|
import { StainlessIslands } from '../components/StainlessIslands';
|
|
14
14
|
|
|
15
15
|
const spec = await cmsClient.getSpec();
|
|
16
|
-
const routes = generateDocsRoutes(spec);
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
export type Props = Awaited<ReturnType<typeof generateDocsRoutes>>[number]['props'] | Record<string, never>;
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const props =
|
|
20
|
+
'language' in Astro.props
|
|
21
|
+
? // In prod, we pass the props down to this page
|
|
22
|
+
Astro.props
|
|
23
|
+
: // In the dev server, we skip the DocsStatic wrapper so we need to find the props ourselves
|
|
24
|
+
generateDocsRoutes(spec).find((r) => r.params.slug === Astro.params.slug)?.props;
|
|
25
|
+
if (!props) throw new Error(`Could not find a route for slug '${Astro.params.slug}'`);
|
|
23
26
|
|
|
24
27
|
// PageTitle override will skip rendering the default Starlight title
|
|
25
28
|
Astro.locals._stlStarlightPage = {
|
|
26
|
-
skipRenderingStarlightTitle:
|
|
29
|
+
skipRenderingStarlightTitle: props.kind === 'readme' ? false : true,
|
|
27
30
|
};
|
|
28
31
|
|
|
29
|
-
const readmeContent = await getReadmeContent(spec,
|
|
30
|
-
const readme =
|
|
32
|
+
const readmeContent = await getReadmeContent(spec, props.language);
|
|
33
|
+
const readme = props.kind === 'readme' ? await astroMarkdownRender(readmeContent ?? '') : null;
|
|
31
34
|
|
|
32
|
-
const resource =
|
|
35
|
+
const resource = props.stainlessPath ? getResourceFromSpec(props.stainlessPath, spec) : null;
|
|
33
36
|
|
|
34
37
|
const pageNav =
|
|
35
|
-
|
|
38
|
+
props.kind === 'resource' && resource
|
|
36
39
|
? buildPageNavigation(resource)
|
|
37
|
-
:
|
|
40
|
+
: props.kind === 'readme' && readme?.metadata
|
|
38
41
|
? readme?.metadata.headings
|
|
39
42
|
: [];
|
|
40
43
|
|
|
41
|
-
const props = route.props;
|
|
42
44
|
Astro.locals.language = props.language;
|
|
43
45
|
|
|
44
46
|
if (readme) {
|
|
@@ -9,6 +9,9 @@ export const getStaticPaths = (async () => {
|
|
|
9
9
|
const routes = generateDocsRoutes(spec);
|
|
10
10
|
return routes;
|
|
11
11
|
}) satisfies GetStaticPaths;
|
|
12
|
+
|
|
13
|
+
export type Props = Awaited<ReturnType<typeof getStaticPaths>>[number]['props'];
|
|
14
|
+
const props = Astro.props;
|
|
12
15
|
---
|
|
13
16
|
|
|
14
|
-
<Docs />
|
|
17
|
+
<Docs {...props} />
|