@stainless-api/docs 0.1.0-beta.137 → 0.1.0-beta.138

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,14 @@
1
1
  # @stainless-api/docs
2
2
 
3
+ ## 0.1.0-beta.138
4
+
5
+ ### Patch Changes
6
+
7
+ - bb5f952: Stop loading AI chat examples from obsolete Stainless endpoint
8
+ - 25d7a11: Stop creating Algolia prose indexes during docs site builds. Vector-based prose indexing is unaffected and remains controlled by the `experimental.disableStainlessProseIndexing` flag.
9
+ - Updated dependencies [25d7a11]
10
+ - @stainless-api/docs-search@0.1.0-beta.49
11
+
3
12
  ## 0.1.0-beta.137
4
13
 
5
14
  ### Minor Changes
@@ -64,10 +64,5 @@
64
64
  "@typescript-eslint/restrict-template-expressions": {
65
65
  "count": 4
66
66
  }
67
- },
68
- "stl-docs/proseSearchIndexing.ts": {
69
- "@typescript-eslint/restrict-template-expressions": {
70
- "count": 1
71
- }
72
67
  }
73
68
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stainless-api/docs",
3
- "version": "0.1.0-beta.137",
3
+ "version": "0.1.0-beta.138",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -72,9 +72,9 @@
72
72
  "shiki": "^4.0.2",
73
73
  "unified": "^11.0.5",
74
74
  "web-worker": "^1.5.0",
75
+ "@stainless-api/docs-search": "0.1.0-beta.49",
75
76
  "@stainless-api/docs-ui": "0.1.0-beta.95",
76
- "@stainless-api/ui-primitives": "0.1.0-beta.53",
77
- "@stainless-api/docs-search": "0.1.0-beta.48"
77
+ "@stainless-api/ui-primitives": "0.1.0-beta.53"
78
78
  },
79
79
  "devDependencies": {
80
80
  "@astrojs/check": "^0.9.9",
@@ -83,13 +83,13 @@
83
83
  "@types/react": "19.2.14",
84
84
  "@types/react-dom": "^19.2.3",
85
85
  "@types/react-syntax-highlighter": "^15.5.13",
86
- "astro": "^6.2.1",
86
+ "astro": "^6.2.2",
87
87
  "react": "^19.2.5",
88
88
  "react-dom": "^19.2.5",
89
89
  "typescript": "6.0.3",
90
90
  "vite": "^7.3.2",
91
91
  "vitest": "^4.1.5",
92
- "zod": "^4.4.2",
92
+ "zod": "^4.4.3",
93
93
  "@stainless/eslint-config": "0.1.0-beta.2",
94
94
  "@stainless/sdk-json": "^0.1.0-beta.10"
95
95
  },
@@ -16,8 +16,6 @@ export function RequestBuilder({
16
16
  }) {
17
17
  let params: Param[];
18
18
  const spec = useSpec();
19
- // https://github.com/Rel1cx/eslint-react/issues/1617
20
- /* eslint-disable @eslint-react/error-boundaries */
21
19
  try {
22
20
  if (!spec) throw new Error('Spec is required for RequestBuilder');
23
21
  params = spec && extractParams(spec, method);
@@ -25,7 +23,6 @@ export function RequestBuilder({
25
23
  console.warn(e);
26
24
  return <div className={className}>{children}</div>;
27
25
  }
28
- /* eslint-enable */
29
26
  const [httpMethod, path] = method.endpoint.split(' ') as [string, string];
30
27
 
31
28
  return (
@@ -10,23 +10,6 @@ export function buildVirtualModuleString<T extends Record<string, unknown>>(vars
10
10
  .join('\n');
11
11
  }
12
12
 
13
- export function makeVirtualModPlugin(bareId: string, content: string): VitePlugin {
14
- return {
15
- name: `stl-virtual-module-loader-${bareId}`,
16
- resolveId(id) {
17
- // The '\0' prefix tells Vite “this is a virtual module” and prevents it from being resolved again.
18
- if (id === bareId) {
19
- return `\0${bareId}`;
20
- }
21
- },
22
- load(id) {
23
- if (id === `\0${bareId}`) {
24
- return content;
25
- }
26
- },
27
- };
28
- }
29
-
30
13
  export function makeAsyncVirtualModPlugin<T extends Record<string, unknown>>(
31
14
  bareId: string,
32
15
  contentLoader: () => Promise<T | string>,
@@ -1,95 +1,31 @@
1
- import { AstroIntegrationLogger } from 'astro';
2
- import z from 'zod';
3
- import {
4
- buildVirtualModuleString,
5
- makeAsyncVirtualModPlugin,
6
- makeVirtualModPlugin,
7
- } from '../shared/virtualModule';
1
+ import { buildVirtualModuleString } from '../shared/virtualModule';
8
2
  import type * as virtualExampleModule from 'virtual:stl-docs-ai-chat-examples';
9
3
  type VirtualExampleModule = typeof virtualExampleModule;
10
4
 
11
- const exampleSchema = z.array(
12
- z.object({
13
- shortPrompt: z.string(),
14
- longPrompt: z.string(),
15
- icon: z.string(),
16
- }),
17
- );
18
- export type ExamplePromptResponse = z.infer<typeof exampleSchema>;
5
+ export type ExamplePromptResponse = {
6
+ shortPrompt: string;
7
+ longPrompt: string;
8
+ icon: string;
9
+ }[];
19
10
 
20
- // handles actually retrieving the information via the Stainless API
21
- async function loadExamples(
22
- projectName: string,
23
- logger: AstroIntegrationLogger,
24
- ): Promise<ExamplePromptResponse | undefined> {
25
- try {
26
- const response = await fetch(`https://api.stainless.com/api/ai/steelie-examples/${projectName}`, {
27
- method: 'GET',
28
- });
29
-
30
- const text = await response.text();
31
- if (!response.ok) {
32
- logger.error(`failed to fetch AI chat examples: ${text}`);
33
- return undefined;
34
- }
35
- const examples = exampleSchema.parse(JSON.parse(text));
36
- return examples;
37
- } catch (error) {
38
- if (error instanceof Error) logger.error(`failed to fetch AI chat examples: ${error.message}`);
39
- else throw error;
40
- return undefined;
41
- }
42
- }
43
-
44
- export default async function generateExamplesPlugin({
45
- projectName,
46
- logger,
47
- exampleOverrides,
48
- }: {
49
- projectName: string | undefined;
50
- logger: AstroIntegrationLogger;
51
- exampleOverrides?: ExamplePromptResponse;
52
- }) {
53
- // if the user has specified any examples, return those immediately
54
- // instead of loading them via the web.
55
- if (exampleOverrides) {
56
- return makeVirtualModPlugin(
57
- 'virtual:stl-docs-ai-chat-examples',
58
- generateVirtualModuleString(exampleOverrides),
59
- );
11
+ export function generateExamplesVirtualModule(exampleOverrides: ExamplePromptResponse | undefined): string {
12
+ if (!exampleOverrides) {
13
+ return buildVirtualModuleString({ examples: undefined } satisfies VirtualExampleModule);
60
14
  }
61
- // if we don't have a defined project name, don't try to fetch examples
62
- if (!projectName) {
63
- return makeVirtualModPlugin(
64
- 'virtual:stl-docs-ai-chat-examples',
65
- buildVirtualModuleString({ examples: undefined } satisfies VirtualExampleModule),
66
- );
67
- }
68
-
69
- // otherwise, promise to get the right examples at some point later on
70
- const examplesPromise = loadExamples(projectName, logger);
71
- return makeAsyncVirtualModPlugin<VirtualExampleModule>('virtual:stl-docs-ai-chat-examples', async () => {
72
- const examples = await examplesPromise;
73
- return generateVirtualModuleString(examples);
74
- });
75
- }
76
-
77
- function generateVirtualModuleString(examples: ExamplePromptResponse | undefined) {
78
- if (!examples) return 'export const examples = undefined;';
79
15
 
80
16
  // Generate icon imports
81
17
  // prettier-ignore
82
18
  const pascalToKebab = (str: string) => str.split(/(?=[A-Z])/).join('-').toLowerCase();
83
19
  const iconImportPath = (iconName: string) =>
84
20
  import.meta.resolve(`lucide-react/dist/esm/icons/${pascalToKebab(iconName)}.js`);
85
- const iconImports = examples.map(
21
+ const iconImports = exampleOverrides.map(
86
22
  ({ icon }) => `import ${icon} from ${JSON.stringify(iconImportPath(icon))}`,
87
23
  );
88
24
 
89
25
  // Reference icon imports in `examples` exported object
90
26
  // "icon":"Sparkles" -> "icon":Sparkles
91
27
  const iconStringsToIdents = (jsonBlob: string) => jsonBlob.replace(/"icon":\s*"(\w+)"/g, '"icon":$1');
92
- const exportBody = `export const examples = ${iconStringsToIdents(JSON.stringify(examples))};`;
28
+ const exportBody = `export const examples = ${iconStringsToIdents(JSON.stringify(exampleOverrides))};`;
93
29
 
94
30
  return [...iconImports, exportBody].join('\n');
95
31
  }
package/stl-docs/index.ts CHANGED
@@ -20,13 +20,12 @@ import {
20
20
  import { buildVirtualModuleString } from '../shared/virtualModule';
21
21
  import { resolveSrcFile } from '../resolveSrcFile';
22
22
  import { stainlessDocsMarkdownRenderer } from './proseMarkdown/proseMarkdownIntegration';
23
- import { getSharedLogger, setSharedLogger } from '../shared/getSharedLogger';
23
+ import { setSharedLogger } from '../shared/getSharedLogger';
24
24
  import { stainlessDocsVectorProseIndexing } from './proseDocSync';
25
- import { stainlessDocsAlgoliaProseIndexing } from './proseSearchIndexing';
26
25
  import { stainlessStarlight } from '../plugin';
27
26
  import { getFontRoles, flattenFonts } from './fonts';
28
27
  import conditionalIntegration from '../shared/conditionalIntegration';
29
- import generateExamplesPlugin from './aiChatExamples';
28
+ import { generateExamplesVirtualModule } from './aiChatExamples';
30
29
  import { ogImageStarlightPlugin } from './og-image';
31
30
 
32
31
  export * from '../plugin';
@@ -172,8 +171,7 @@ function stainlessDocsIntegration(
172
171
  return {
173
172
  name: 'stl-docs-astro',
174
173
  hooks: {
175
- 'astro:config:setup': async ({ updateConfig, command, config: astroConfig, logger: localLogger }) => {
176
- const logger = getSharedLogger({ fallback: localLogger });
174
+ 'astro:config:setup': ({ updateConfig, command, config: astroConfig }) => {
177
175
  // we only handle redirects for builds
178
176
  // in dev, Astro handles them for us
179
177
  if (command === 'build' && astroConfig.redirects) {
@@ -195,30 +193,35 @@ function stainlessDocsIntegration(
195
193
  vmAiChatHandlerExport = `export { default as AI_CHAT_HANDLER } from '${handlerEntrypoint}';`;
196
194
  }
197
195
 
198
- const virtualModules = new Map(
199
- Object.entries({
200
- 'virtual:stl-docs-virtual-module': [
201
- buildVirtualModuleString({
202
- TABS: config.tabs.map((tab) => ({ ...tab, link: withBase(tab.link) })),
203
- SPLIT_TABS_ENABLED: config.splitTabsEnabled,
204
- HEADER_LINKS: config.header.links.map((link) => ({ ...link, link: withBase(link.link) })),
205
- HEADER_LAYOUT: config.header.layout,
206
- ENABLE_CLIENT_ROUTER: config.enableClientRouter,
207
- API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
208
- ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
209
- ENABLE_CONTEXT_MENU: !!config.contextMenu, // TODO: do not duplicate this between both virtual modules
210
- CONTEXT_MENU_ENABLE_THIRD_PARTY:
211
- (typeof config.contextMenu === 'object' ? config.contextMenu.thirdParty : null) ?? true,
212
- RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
213
- FONTS: getFontRoles(config.fonts),
214
- LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: config.linkGroupTitlesToOverviewPages,
215
- RENDER_CREDITS: config.credits,
216
- SITE_TITLE: config.siteTitle,
217
- }),
218
- ].join('\n'),
219
- 'virtual:stl-docs-ai-chat': vmAiChatHandlerExport,
220
- }),
221
- );
196
+ const virtualModules = new Map<string, string>([
197
+ [
198
+ 'virtual:stl-docs-virtual-module',
199
+ buildVirtualModuleString({
200
+ TABS: config.tabs.map((tab) => ({ ...tab, link: withBase(tab.link) })),
201
+ SPLIT_TABS_ENABLED: config.splitTabsEnabled,
202
+ HEADER_LINKS: config.header.links.map((link) => ({ ...link, link: withBase(link.link) })),
203
+ HEADER_LAYOUT: config.header.layout,
204
+ ENABLE_CLIENT_ROUTER: config.enableClientRouter,
205
+ API_REFERENCE_BASE_PATH: apiReferenceBasePath ?? '/api',
206
+ ENABLE_PROSE_MARKDOWN_RENDERING: config.enableProseMarkdownRendering,
207
+ ENABLE_CONTEXT_MENU: !!config.contextMenu, // TODO: do not duplicate this between both virtual modules
208
+ CONTEXT_MENU_ENABLE_THIRD_PARTY:
209
+ (typeof config.contextMenu === 'object' ? config.contextMenu.thirdParty : null) ?? true,
210
+ RENDER_PAGE_DESCRIPTIONS: config.renderPageDescriptions,
211
+ FONTS: getFontRoles(config.fonts),
212
+ LINK_GROUP_TITLES_TO_OVERVIEW_PAGES: config.linkGroupTitlesToOverviewPages,
213
+ RENDER_CREDITS: config.credits,
214
+ SITE_TITLE: config.siteTitle,
215
+ }),
216
+ ],
217
+ ['virtual:stl-docs-ai-chat', vmAiChatHandlerExport],
218
+ ]);
219
+ if (config.aiChat) {
220
+ virtualModules.set(
221
+ 'virtual:stl-docs-ai-chat-examples',
222
+ generateExamplesVirtualModule(config.aiChat.examples),
223
+ );
224
+ }
222
225
 
223
226
  updateConfig({
224
227
  fonts: [...flattenFonts(config.fonts), ...(astroConfig?.fonts ?? [])],
@@ -239,17 +242,6 @@ function stainlessDocsIntegration(
239
242
  if (virtualModules.has(bare)) return virtualModules.get(bare);
240
243
  },
241
244
  },
242
- // Separate plugin for the examples because it has async resolution; not a simple string
243
- // like the above plugins
244
- ...(config.aiChat
245
- ? [
246
- await generateExamplesPlugin({
247
- projectName: config.apiReference?.stainlessProject ?? undefined,
248
- logger,
249
- exampleOverrides: config.aiChat.examples,
250
- }),
251
- ]
252
- : []),
253
245
  ],
254
246
  },
255
247
  build: {
@@ -311,11 +303,6 @@ export function stainlessDocs(config: StainlessDocsUserConfig): AstroIntegration
311
303
  integration: stainlessDocsMarkdownRenderer({ apiReferenceBasePath }),
312
304
  reason: 'disabled by experimental config "disableProseMarkdownRendering"',
313
305
  }),
314
- conditionalIntegration({
315
- condition: !config.experimental?.disableStainlessProseIndexing,
316
- integration: stainlessDocsAlgoliaProseIndexing({ apiReferenceBasePath }),
317
- reason: 'disabled by experimental config "disableStainlessProseIndexing"',
318
- }),
319
306
  conditionalIntegration({
320
307
  condition: !config.experimental?.disableStainlessProseIndexing,
321
308
  integration: stainlessDocsVectorProseIndexing(normalizedConfig, apiReferenceBasePath),
@@ -1,218 +0,0 @@
1
- import type { AstroIntegration } from 'astro';
2
- import { readFile } from 'fs/promises';
3
- import { getProsePages } from '../shared/getProsePages';
4
- import { getSharedLogger } from '../shared/getSharedLogger';
5
- import { bold } from '../shared/terminalUtils';
6
- import * as cheerio from 'cheerio';
7
- import { buildProseIndex } from '@stainless-api/docs-search/providers/algolia';
8
-
9
- class SectionContext {
10
- headers: { level: number; text: string }[] = [];
11
- headerId: string | undefined;
12
- headerTag: string | undefined;
13
- headerText: string | undefined;
14
- hasContent = false;
15
-
16
- get(): string | undefined {
17
- if (this.headers.length === 0) return;
18
- return this.headers.map((h) => h.text).join(' > ');
19
- }
20
-
21
- header({ id, tag, text }: { id: string; tag: string; text: string }) {
22
- const level = getHeaderLevel(tag);
23
- if (level > 0) {
24
- while (this.headers.length > 0 && this.headers[this.headers.length - 1]!.level >= level) {
25
- this.headers.pop();
26
- }
27
- this.headers.push({ level, text });
28
- }
29
- this.headerId = id;
30
- this.headerTag = tag;
31
- this.headerText = text;
32
- this.hasContent = false;
33
- }
34
- }
35
-
36
- function slugify(text: string): string {
37
- return text
38
- .toLowerCase()
39
- .replace(/`/g, '')
40
- .replace(/[^a-z0-9]+/g, '-')
41
- .replace(/^-|-$/g, '');
42
- }
43
-
44
- function getHeaderLevel(tag: string): number {
45
- const match = tag.match(/^h(\d)$/);
46
- return match ? parseInt(match[1]!, 10) : 0;
47
- }
48
-
49
- // Chunking configuration
50
- // We target 64-256 tokens per chunk, using ~1.3 tokens/word for English text
51
- const TOKENS_PER_WORD = 1.3;
52
- const MIN_TOKENS = 64;
53
- const MAX_TOKENS = 256;
54
- const MIN_WORDS = Math.floor(MIN_TOKENS / TOKENS_PER_WORD); // ~49 words
55
- const MAX_WORDS = Math.floor(MAX_TOKENS / TOKENS_PER_WORD); // ~197 words
56
- const SENTENCE_BREAK_WORDS = Math.floor((MAX_TOKENS * 0.875) / TOKENS_PER_WORD); // ~172 words
57
-
58
- /**
59
- * Chunks text content into segments of 64-256 tokens using word-based boundaries.
60
- * Prefers breaking at sentence endings for natural chunk boundaries.
61
- */
62
- function chunkTextByWords(text: string): string[] {
63
- const words = text.split(/\s+/).filter((w) => w.length > 0);
64
-
65
- if (words.length <= MAX_WORDS) {
66
- return words.length > 0 ? [words.join(' ')] : [];
67
- }
68
-
69
- const chunks: string[] = [];
70
- let currentChunk: string[] = [];
71
-
72
- for (const word of words) {
73
- currentChunk.push(word);
74
-
75
- // Force break at max words
76
- if (currentChunk.length >= MAX_WORDS) {
77
- chunks.push(currentChunk.join(' '));
78
- currentChunk = [];
79
- continue;
80
- }
81
-
82
- // Prefer breaking at sentence boundaries after threshold
83
- if (currentChunk.length >= SENTENCE_BREAK_WORDS && /[.!?]["']?$/.test(word)) {
84
- chunks.push(currentChunk.join(' '));
85
- currentChunk = [];
86
- }
87
- }
88
-
89
- if (currentChunk.length > 0) {
90
- if (currentChunk.length < MIN_WORDS && chunks.length > 0) {
91
- const lastChunk = chunks[chunks.length - 1]!;
92
- const mergedWords = lastChunk.split(/\s+/).length + currentChunk.length;
93
- if (mergedWords <= MAX_WORDS) {
94
- chunks[chunks.length - 1] = lastChunk + ' ' + currentChunk.join(' ');
95
- } else {
96
- chunks.push(currentChunk.join(' '));
97
- }
98
- } else {
99
- chunks.push(currentChunk.join(' '));
100
- }
101
- }
102
-
103
- return chunks;
104
- }
105
-
106
- type IndexEntry = {
107
- chunk: { id: string; index: number; total: number };
108
- id: string;
109
- tag: string;
110
- content: string;
111
- language?: string;
112
- sectionContext?: string;
113
- };
114
-
115
- const DEFAULT_ROOT = 'main';
116
- const DEFAULT_PATTERN = 'h1, h2, h3, h4, h5, h6, p, li, pre code';
117
-
118
- /**
119
- * Indexes HTML content for search, with section context and code language extraction.
120
- *
121
- * Features:
122
- * - Tracks header hierarchy to prepend section context (e.g., "Guide > Setup: ...")
123
- * - Extracts language metadata from code blocks (class="language-js")
124
- * - Uses word-based chunking with sentence boundary detection
125
- */
126
- function* indexHTML(content: string, root = DEFAULT_ROOT, pattern = DEFAULT_PATTERN): Generator<IndexEntry> {
127
- const $ = cheerio.load(content);
128
- const matches = $(root).find(pattern);
129
-
130
- const ctx = new SectionContext();
131
-
132
- for (const match of matches) {
133
- const tagName = match.tagName.toLowerCase();
134
- const rawText = $(match).text().trim();
135
-
136
- if (getHeaderLevel(tagName) > 0) {
137
- ctx.header({ id: $(match).attr('id') ?? slugify(rawText), tag: tagName, text: rawText });
138
- continue;
139
- }
140
-
141
- // Check if this is a code block and extract language
142
- const isCode = tagName === 'code' && $(match).parent().is('pre');
143
- let language: string | undefined;
144
- if (isCode) {
145
- const classes = $(match).attr('class') || '';
146
- const langMatch = classes.match(/(?:language-|lang-)([a-zA-Z0-9+-]+)/);
147
- language = langMatch ? langMatch[1] : undefined;
148
- }
149
-
150
- // Build content with section context
151
- const sectionContext = ctx.get();
152
- const chunks = chunkTextByWords(rawText);
153
- const chunkId = crypto.randomUUID();
154
-
155
- for (const [chunkN, chunkText] of chunks.entries()) {
156
- yield {
157
- id: ctx.headerId ?? $(match).attr('id') ?? chunkId,
158
- tag: isCode ? 'code' : tagName,
159
- content: chunkText,
160
- ...(sectionContext ? { sectionContext } : {}),
161
- ...(language && { language }),
162
- chunk: {
163
- id: chunkId,
164
- index: chunkN,
165
- total: chunks.length,
166
- },
167
- };
168
- ctx.hasContent = true;
169
- }
170
- }
171
- }
172
-
173
- export function stainlessDocsAlgoliaProseIndexing({
174
- apiReferenceBasePath,
175
- }: {
176
- apiReferenceBasePath: string | null;
177
- }): AstroIntegration {
178
- return {
179
- name: 'stl-docs-prose-indexing',
180
- hooks: {
181
- 'astro:build:done': async ({ logger: localLogger, dir }) => {
182
- const logger = getSharedLogger({ fallback: localLogger });
183
- const outputBasePath = dir.pathname;
184
-
185
- const {
186
- PUBLIC_ALGOLIA_APP_ID: appId,
187
- PUBLIC_ALGOLIA_INDEX: indexName,
188
- PRIVATE_ALGOLIA_WRITE_KEY: algoliaWriteKey,
189
- } = process.env;
190
-
191
- if (!appId || !indexName || !algoliaWriteKey) {
192
- logger.info('Skipping algolia indexing due to missing environment variables');
193
- return;
194
- }
195
-
196
- const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
197
- logger.info(bold(`Indexing ${pagesToRender.length} prose pages for algolia search`));
198
-
199
- const objects = [];
200
- for (const absHtmlPath of pagesToRender) {
201
- const content = await readFile(absHtmlPath, 'utf-8');
202
- const idx = indexHTML(content);
203
- for (const entry of idx)
204
- objects.push({
205
- ...entry,
206
- source: absHtmlPath.slice(outputBasePath.length),
207
- });
208
- }
209
-
210
- try {
211
- await buildProseIndex(appId, `${indexName}-prose`, algoliaWriteKey, objects);
212
- } catch (err) {
213
- logger.error(`Failed to index prose content: ${err}`);
214
- }
215
- },
216
- },
217
- };
218
- }