@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 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
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "plugin/cms/server.ts": {
13
13
  "@typescript-eslint/no-explicit-any": {
14
- "count": 4
14
+ "count": 3
15
15
  }
16
16
  },
17
17
  "plugin/cms/sidebar-builder.ts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stainless-api/docs",
3
- "version": "0.1.0-beta.82",
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.13",
62
- "@stainless-api/docs-ui": "0.1.0-beta.60",
63
- "@stainless-api/ui-primitives": "0.1.0-beta.44"
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",
@@ -74,7 +74,7 @@ export async function buildAlgoliaIndex({
74
74
  oas: transformedOAS,
75
75
  config,
76
76
  languages,
77
- version,
77
+ projectName: version.stainlessProject,
78
78
  });
79
79
 
80
80
  const {
@@ -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
+ }
@@ -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 { createSDKJSON, parseInputs, transformOAS } from './worker';
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
- }): Promise<{ data: SDKJSON.Spec; auth: Auth; id: string }> {
73
- async function unsafeLoad(): Promise<{ data: SDKJSON.Spec; auth: Auth; id: string }> {
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 { oas, config } = await parseInputs({
100
- oas: oasStr,
101
- config: configStr,
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.security_schemes.map((scheme) => ({
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<any> {
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 { stainlessPath: currentStainlessPath, language } = parseRoute(body.basePath, body.currentSlug);
286
-
287
- const configItemsBuilder = new SidebarConfigItemsBuilder(
288
- currentSpec.data,
289
- language,
290
- sidebarGenerateOptions,
291
- );
292
-
293
- let userSidebarConfig = configItemsBuilder.generateItems();
294
-
295
- if (sidebarConfig && sidebarConfig.transformFn) {
296
- const transformedSidebarConfig = sidebarConfig.transformFn(userSidebarConfig, language);
297
- // the user may not have returned, but they may have mutated the config in place
298
- // if they did return, we use that
299
- if (transformedSidebarConfig) {
300
- userSidebarConfig = transformedSidebarConfig;
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 = BuildSidebarParams & {
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: currentSlug === readmeSlug,
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
  }),
@@ -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
- version,
163
+ projectName,
165
164
  }: {
166
165
  oas: OpenAPIDocument;
167
166
  config: ParsedConfig;
168
167
  languages: DocsLanguage[];
169
- version: VersionUserConfig;
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: version.stainlessProject,
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
- const cmsServer = startDevServer({
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.invalidate();
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.destroy();
356
+ await cmsServer?.destroy();
345
357
  },
346
358
  'astro:build:start'({ logger }) {
347
359
  collectedErrors = [];
@@ -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
- const route = routes.find((r) => r.params.slug === Astro.params.slug);
17
+ export type Props = Awaited<ReturnType<typeof generateDocsRoutes>>[number]['props'] | Record<string, never>;
19
18
 
20
- if (!route) {
21
- throw new Error(`Could not find a route for slug '${Astro.params.slug}'`);
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: route.props.kind === 'readme' ? false : true,
29
+ skipRenderingStarlightTitle: props.kind === 'readme' ? false : true,
27
30
  };
28
31
 
29
- const readmeContent = await getReadmeContent(spec, route.props.language);
30
- const readme = route.props.kind === 'readme' ? await astroMarkdownRender(readmeContent ?? '') : null;
32
+ const readmeContent = await getReadmeContent(spec, props.language);
33
+ const readme = props.kind === 'readme' ? await astroMarkdownRender(readmeContent ?? '') : null;
31
34
 
32
- const resource = route.props.stainlessPath ? getResourceFromSpec(route.props.stainlessPath, spec) : null;
35
+ const resource = props.stainlessPath ? getResourceFromSpec(props.stainlessPath, spec) : null;
33
36
 
34
37
  const pageNav =
35
- route.props.kind === 'resource' && resource
38
+ props.kind === 'resource' && resource
36
39
  ? buildPageNavigation(resource)
37
- : route.props.kind === 'readme' && readme?.metadata
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} />