@stainless-api/docs 0.1.0-beta.99 → 1.0.0-beta.140
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 +390 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +22 -6
- package/{eslint.config.js → eslint.config.ts} +1 -7
- package/package.json +57 -40
- package/plugin/buildAlgoliaIndex.ts +6 -12
- package/plugin/components/SDKSelect.astro +0 -6
- package/plugin/components/SnippetCode.tsx +6 -37
- package/plugin/components/search/SearchAlgolia.astro +1 -1
- package/plugin/components/search/SearchIsland.tsx +19 -13
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/globalJs/ai-dropdown-options.ts +22 -9
- package/plugin/globalJs/code-snippets.ts +5 -5
- package/plugin/globalJs/copy.ts +20 -91
- package/plugin/globalJs/navigation.ts +13 -13
- package/plugin/globalJs/summary-selection-tweak.ts +29 -0
- package/plugin/index.ts +107 -163
- package/plugin/loadPluginConfig.ts +49 -151
- package/plugin/markdown/highlighter.ts +100 -0
- package/plugin/markdown/index.ts +39 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +2 -0
- package/plugin/react/Routing.tsx +10 -244
- package/plugin/referencePlaceholderUtils.ts +1 -1
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +1 -1
- package/plugin/routes/Docs.astro +3 -1
- package/plugin/routes/Overview.astro +14 -7
- package/plugin/routes/llms.ts +186 -0
- package/plugin/routes/markdown.ts +62 -13
- package/plugin/sidebar-utils/sidebar-builder.ts +38 -12
- package/plugin/specs/defaultSpecLoader.ts +192 -0
- package/plugin/specs/fetchSpecSSR.ts +1 -1
- package/plugin/specs/utils.ts +86 -0
- package/shared/conditionalIntegration.ts +28 -0
- package/shared/getProsePages.ts +6 -7
- package/shared/virtualModule.ts +1 -26
- package/stl-docs/aiChatExamples.ts +31 -0
- package/stl-docs/chat/docs-chat-handler.ts +17 -0
- package/stl-docs/chat/hook.ts +225 -0
- package/stl-docs/chat/schemas.ts +27 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +175 -0
- package/stl-docs/chat/ui/Trigger.tsx +154 -0
- package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
- package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
- package/stl-docs/chat/ui/components/ChatLog.tsx +93 -0
- package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
- package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +106 -0
- package/stl-docs/chat/ui/components/Table.tsx +15 -0
- package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
- package/stl-docs/chat/ui/components/hljs-github.css +81 -0
- package/stl-docs/chat/ui/scroll-manager.ts +86 -0
- package/stl-docs/chat/ui/types.ts +45 -0
- package/stl-docs/components/AiChatIsland.tsx +10 -12
- package/stl-docs/components/ContentPanel.astro +9 -0
- package/stl-docs/components/Footer.astro +89 -0
- package/stl-docs/components/Header.astro +0 -5
- package/stl-docs/components/PageFrame.astro +23 -8
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/StainlessLogo.svg +4 -0
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/headers/DefaultHeader.astro +6 -8
- package/stl-docs/components/headers/StackedHeader.astro +5 -53
- package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
- package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
- package/stl-docs/components/mintlify-compat/Frame.astro +2 -2
- package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +12 -7
- package/stl-docs/components/nav-tabs/NavTabs.astro +5 -3
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +2 -0
- package/stl-docs/components/pagination/Pagination.astro +4 -2
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +2 -2
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +2 -2
- package/stl-docs/components/pagination/util.ts +3 -3
- package/stl-docs/components/sidebars/BaseSidebar.astro +72 -1
- package/stl-docs/disableCalloutSyntax.ts +1 -1
- package/stl-docs/fonts.ts +5 -5
- package/stl-docs/index.ts +76 -53
- package/stl-docs/loadStlDocsConfig.ts +38 -8
- package/stl-docs/og-image/components/OpenGraphFunctionSignature.tsx +64 -0
- package/stl-docs/og-image/components/OpenGraphImage.tsx +126 -0
- package/stl-docs/og-image/config.ts +56 -0
- package/stl-docs/og-image/image-gen/generate-api-reference-og-image.tsx +188 -0
- package/stl-docs/og-image/image-gen/generate-og-image.tsx +119 -0
- package/stl-docs/og-image/image-gen/get-logo-url.ts +47 -0
- package/stl-docs/og-image/index.ts +135 -0
- package/stl-docs/og-image/routes/add-og-image.ts +45 -0
- package/stl-docs/og-image/routes/get-api-reference-og-image.ts +36 -0
- package/stl-docs/og-image/routes/get-og-image.ts +28 -0
- package/stl-docs/og-image/theme.ts +43 -0
- package/stl-docs/og-image/utils.ts +14 -0
- package/stl-docs/proseDocSync.test.ts +74 -0
- package/stl-docs/proseDocSync.ts +344 -0
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +4 -12
- package/stl-docs/schema-extension.ts +12 -0
- package/stl-docs/tabsMiddleware.ts +1 -1
- package/styles/overrides.css +2 -14
- package/styles/page.css +210 -71
- package/styles/sidebar.css +30 -17
- package/styles/sl-variables.css +3 -8
- package/styles/stldocs-variables.css +2 -2
- package/styles/toc.css +8 -0
- package/tsconfig.json +1 -1
- package/virtual-module.d.ts +35 -11
- package/playground-virtual-modules.d.ts +0 -96
- package/plugin/globalJs/create-playground.shim.ts +0 -3
- package/plugin/globalJs/playground-data.shim.ts +0 -1
- package/plugin/globalJs/playground-data.ts +0 -14
- package/plugin/specs/FileCache.ts +0 -99
- package/plugin/specs/generateSpec.ts +0 -112
- package/plugin/specs/index.ts +0 -132
- package/plugin/specs/inputResolver.ts +0 -146
- package/plugin/specs/worker.ts +0 -199
- package/plugin/vendor/preview.worker.docs.js +0 -26108
- package/plugin/vendor/templates/cli.md +0 -1
- package/plugin/vendor/templates/go.md +0 -316
- package/plugin/vendor/templates/java.md +0 -89
- package/plugin/vendor/templates/kotlin.md +0 -89
- 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
- package/stl-docs/components/ClientRouterHead.astro +0 -41
- package/stl-docs/components/content-panel/ContentPanel.astro +0 -42
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -65
- package/stl-docs/proseSearchIndexing.ts +0 -606
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { batchBySize, MAX_BATCH_BYTES, MAX_BATCH_DOCS } from './proseDocSync';
|
|
3
|
+
|
|
4
|
+
function makeDoc(docId: string, sizeBytes: number) {
|
|
5
|
+
return {
|
|
6
|
+
docId,
|
|
7
|
+
content: Buffer.alloc(sizeBytes, 'x'),
|
|
8
|
+
sha256: 'fake',
|
|
9
|
+
source: `/${docId}`,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('batchBySize', () => {
|
|
14
|
+
it('returns empty array for no docs', () => {
|
|
15
|
+
expect(batchBySize([])).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('puts all docs in one batch when under both limits', () => {
|
|
19
|
+
const docs = Array.from({ length: 5 }, (_, i) => makeDoc(`doc-${i}`, 100));
|
|
20
|
+
const batches = batchBySize(docs);
|
|
21
|
+
expect(batches).toHaveLength(1);
|
|
22
|
+
expect(batches[0]).toHaveLength(5);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('splits at MAX_BATCH_DOCS', () => {
|
|
26
|
+
const docs = Array.from({ length: MAX_BATCH_DOCS + 10 }, (_, i) => makeDoc(`doc-${i}`, 10));
|
|
27
|
+
const batches = batchBySize(docs);
|
|
28
|
+
expect(batches).toHaveLength(2);
|
|
29
|
+
expect(batches[0]).toHaveLength(MAX_BATCH_DOCS);
|
|
30
|
+
expect(batches[1]).toHaveLength(10);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('splits when cumulative size exceeds MAX_BATCH_BYTES', () => {
|
|
34
|
+
const docSize = 10 * 1024 * 1024; // 10MB each
|
|
35
|
+
// 4 docs = 40MB > 30MB limit, so should split into [3, 1]
|
|
36
|
+
const docs = Array.from({ length: 4 }, (_, i) => makeDoc(`doc-${i}`, docSize));
|
|
37
|
+
const batches = batchBySize(docs);
|
|
38
|
+
expect(batches).toHaveLength(2);
|
|
39
|
+
expect(batches[0]).toHaveLength(3);
|
|
40
|
+
expect(batches[1]).toHaveLength(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('handles a single doc larger than MAX_BATCH_BYTES', () => {
|
|
44
|
+
const docs = [makeDoc('huge', MAX_BATCH_BYTES + 1)];
|
|
45
|
+
const batches = batchBySize(docs);
|
|
46
|
+
// Still goes into a batch on its own — we can't split a single doc
|
|
47
|
+
expect(batches).toHaveLength(1);
|
|
48
|
+
expect(batches[0]).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('byte limit takes precedence over doc count when hit first', () => {
|
|
52
|
+
// 2 docs of 20MB each = 40MB > 30MB limit, well under 100 doc limit
|
|
53
|
+
const docs = [makeDoc('a', 20 * 1024 * 1024), makeDoc('b', 20 * 1024 * 1024)];
|
|
54
|
+
const batches = batchBySize(docs);
|
|
55
|
+
expect(batches).toHaveLength(2);
|
|
56
|
+
expect(batches[0]).toHaveLength(1);
|
|
57
|
+
expect(batches[1]).toHaveLength(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('creates multiple batches for many large docs', () => {
|
|
61
|
+
const docSize = 8 * 1024 * 1024; // 8MB each
|
|
62
|
+
// 10 docs * 8MB = 80MB, should split into batches of ~3 (24MB each)
|
|
63
|
+
const docs = Array.from({ length: 10 }, (_, i) => makeDoc(`doc-${i}`, docSize));
|
|
64
|
+
const batches = batchBySize(docs);
|
|
65
|
+
// Every batch should be <= MAX_BATCH_BYTES
|
|
66
|
+
for (const batch of batches) {
|
|
67
|
+
const totalBytes = batch.reduce((sum, d) => sum + d.content.byteLength, 0);
|
|
68
|
+
expect(totalBytes).toBeLessThanOrEqual(MAX_BATCH_BYTES);
|
|
69
|
+
}
|
|
70
|
+
// All docs should be accounted for
|
|
71
|
+
const totalDocs = batches.reduce((sum, b) => sum + b.length, 0);
|
|
72
|
+
expect(totalDocs).toBe(10);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import type { AstroIntegration, AstroIntegrationLogger } from 'astro';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { readFile } from 'fs/promises';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getProsePages } from '../shared/getProsePages';
|
|
6
|
+
import { getSharedLogger } from '../shared/getSharedLogger';
|
|
7
|
+
import { bold } from '../shared/terminalUtils';
|
|
8
|
+
import { NormalizedStainlessDocsConfig } from './loadStlDocsConfig';
|
|
9
|
+
|
|
10
|
+
const DOCS_API_BASE_URL = 'https://api.stainlessapi.com';
|
|
11
|
+
|
|
12
|
+
export const MAX_BATCH_DOCS = 100;
|
|
13
|
+
// Cloud Run has a 32MB request body limit; leave headroom for JSON envelope overhead
|
|
14
|
+
export const MAX_BATCH_BYTES = 30 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
// ─── API client ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
async function docsApiRequest(
|
|
19
|
+
method: string,
|
|
20
|
+
apiPath: string,
|
|
21
|
+
apiKey: string,
|
|
22
|
+
body?: object,
|
|
23
|
+
): Promise<Response> {
|
|
24
|
+
const headers: Record<string, string> = {
|
|
25
|
+
Authorization: `Bearer ${apiKey}`,
|
|
26
|
+
};
|
|
27
|
+
if (body) {
|
|
28
|
+
headers['Content-Type'] = 'application/json';
|
|
29
|
+
}
|
|
30
|
+
return fetch(`${DOCS_API_BASE_URL}${apiPath}`, {
|
|
31
|
+
method,
|
|
32
|
+
headers,
|
|
33
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Manifest & diffing ─────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
type LocalDoc = { content: Buffer; sha256: string; source: string };
|
|
40
|
+
|
|
41
|
+
function sha256(content: Buffer): string {
|
|
42
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function buildLocalManifest(pages: string[], outputBasePath: string): Promise<Map<string, LocalDoc>> {
|
|
46
|
+
const docs = new Map<string, LocalDoc>();
|
|
47
|
+
for (const absHtmlPath of pages) {
|
|
48
|
+
const content = await readFile(absHtmlPath);
|
|
49
|
+
const docId = path.relative(outputBasePath, absHtmlPath);
|
|
50
|
+
docs.set(docId, { content, sha256: sha256(content), source: '/' + docId });
|
|
51
|
+
}
|
|
52
|
+
return docs;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchRemoteManifest(
|
|
56
|
+
docsSiteId: string,
|
|
57
|
+
project: string,
|
|
58
|
+
apiKey: string,
|
|
59
|
+
logger: AstroIntegrationLogger,
|
|
60
|
+
): Promise<Map<string, string>> {
|
|
61
|
+
try {
|
|
62
|
+
const response = await docsApiRequest(
|
|
63
|
+
'GET',
|
|
64
|
+
`/api/docs-sites/${docsSiteId}/documents?project=${encodeURIComponent(project)}`,
|
|
65
|
+
apiKey,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (response.ok) {
|
|
69
|
+
const data = (await response.json()) as {
|
|
70
|
+
documents: { id: string; content_sha256: string }[];
|
|
71
|
+
};
|
|
72
|
+
return new Map(data.documents.map((d) => [d.id, d.content_sha256]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.error(`Failed to list remote documents (HTTP ${response.status}): ${await response.text()}`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger.error(`Failed to list remote documents: ${err}`);
|
|
78
|
+
}
|
|
79
|
+
return new Map();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function diffManifests(
|
|
83
|
+
localDocs: Map<string, LocalDoc>,
|
|
84
|
+
remoteDocs: Map<string, string>,
|
|
85
|
+
): { toPut: (LocalDoc & { docId: string })[]; toDelete: string[] } {
|
|
86
|
+
const toPut: (LocalDoc & { docId: string })[] = [];
|
|
87
|
+
for (const [docId, local] of localDocs) {
|
|
88
|
+
if (remoteDocs.get(docId) !== local.sha256) {
|
|
89
|
+
toPut.push({ docId, ...local });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const toDelete: string[] = [];
|
|
94
|
+
for (const remoteDocId of remoteDocs.keys()) {
|
|
95
|
+
if (!localDocs.has(remoteDocId)) {
|
|
96
|
+
toDelete.push(remoteDocId);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { toPut, toDelete };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Batching ────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
export function batchBySize(docs: (LocalDoc & { docId: string })[]): (LocalDoc & { docId: string })[][] {
|
|
106
|
+
const batches: (LocalDoc & { docId: string })[][] = [];
|
|
107
|
+
let current: (LocalDoc & { docId: string })[] = [];
|
|
108
|
+
let currentBytes = 0;
|
|
109
|
+
|
|
110
|
+
for (const doc of docs) {
|
|
111
|
+
const docBytes = doc.content.byteLength;
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
current.length > 0 &&
|
|
115
|
+
(current.length >= MAX_BATCH_DOCS || currentBytes + docBytes > MAX_BATCH_BYTES)
|
|
116
|
+
) {
|
|
117
|
+
batches.push(current);
|
|
118
|
+
current = [];
|
|
119
|
+
currentBytes = 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
current.push(doc);
|
|
123
|
+
currentBytes += docBytes;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (current.length > 0) batches.push(current);
|
|
127
|
+
return batches;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Import & delete ────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
type ImportJobResult = { succeeded: number; failed: number; errors: string[] };
|
|
133
|
+
|
|
134
|
+
async function importDocuments(
|
|
135
|
+
docs: (LocalDoc & { docId: string })[],
|
|
136
|
+
docsSiteId: string,
|
|
137
|
+
project: string,
|
|
138
|
+
apiKey: string,
|
|
139
|
+
logger: AstroIntegrationLogger,
|
|
140
|
+
): Promise<ImportJobResult> {
|
|
141
|
+
const totals: ImportJobResult = { succeeded: 0, failed: 0, errors: [] };
|
|
142
|
+
const batches = batchBySize(docs);
|
|
143
|
+
|
|
144
|
+
for (const batch of batches) {
|
|
145
|
+
let response: Response;
|
|
146
|
+
try {
|
|
147
|
+
response = await docsApiRequest('POST', `/api/docs-sites/${docsSiteId}/documents/import`, apiKey, {
|
|
148
|
+
project,
|
|
149
|
+
documents: batch.map(({ docId, content, source }) => ({
|
|
150
|
+
id: docId,
|
|
151
|
+
content: content.toString('utf-8'),
|
|
152
|
+
content_type: 'text/html',
|
|
153
|
+
source,
|
|
154
|
+
})),
|
|
155
|
+
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
logger.error(`Failed to submit import batch: ${err}`);
|
|
158
|
+
totals.failed += batch.length;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
logger.error(`Failed to submit import batch (HTTP ${response.status}): ${await response.text()}`);
|
|
164
|
+
totals.failed += batch.length;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const { job_id } = (await response.json()) as { job_id: string };
|
|
169
|
+
const result = await pollImportJob(docsSiteId, job_id, apiKey, logger);
|
|
170
|
+
totals.succeeded += result.succeeded;
|
|
171
|
+
totals.failed += result.failed;
|
|
172
|
+
totals.errors.push(...result.errors);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return totals;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function pollImportJob(
|
|
179
|
+
docsSiteId: string,
|
|
180
|
+
jobId: string,
|
|
181
|
+
apiKey: string,
|
|
182
|
+
logger: AstroIntegrationLogger,
|
|
183
|
+
): Promise<ImportJobResult> {
|
|
184
|
+
const maxWait = 5 * 60_000;
|
|
185
|
+
const start = Date.now();
|
|
186
|
+
|
|
187
|
+
while (Date.now() - start < maxWait) {
|
|
188
|
+
await new Promise((r) => setTimeout(r, 2_000));
|
|
189
|
+
|
|
190
|
+
let response: Response;
|
|
191
|
+
try {
|
|
192
|
+
response = await docsApiRequest('GET', `/api/docs-sites/${docsSiteId}/documents/jobs/${jobId}`, apiKey);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
logger.error(`Failed to poll import job ${jobId}: ${err}`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
logger.error(`Failed to poll import job ${jobId} (HTTP ${response.status}): ${await response.text()}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const job = (await response.json()) as {
|
|
204
|
+
status: string;
|
|
205
|
+
succeeded: number;
|
|
206
|
+
failed: number;
|
|
207
|
+
errors: string[] | null;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (job.status === 'queued' || job.status === 'processing') continue;
|
|
211
|
+
|
|
212
|
+
return { succeeded: job.succeeded, failed: job.failed, errors: job.errors ?? [] };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
logger.error(`Import job ${jobId} timed out after ${maxWait / 1000}s`);
|
|
216
|
+
return { succeeded: 0, failed: 0, errors: [`Job ${jobId} timed out`] };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function deleteDocuments(
|
|
220
|
+
docIds: string[],
|
|
221
|
+
docsSiteId: string,
|
|
222
|
+
project: string,
|
|
223
|
+
apiKey: string,
|
|
224
|
+
logger: AstroIntegrationLogger,
|
|
225
|
+
): Promise<{ succeeded: number; failed: number }> {
|
|
226
|
+
let succeeded = 0;
|
|
227
|
+
let failed = 0;
|
|
228
|
+
|
|
229
|
+
await Promise.all(
|
|
230
|
+
docIds.map(async (docId) => {
|
|
231
|
+
try {
|
|
232
|
+
const response = await docsApiRequest(
|
|
233
|
+
'DELETE',
|
|
234
|
+
`/api/docs-sites/${docsSiteId}/documents?documentId=${encodeURIComponent(docId)}&project=${encodeURIComponent(project)}`,
|
|
235
|
+
apiKey,
|
|
236
|
+
);
|
|
237
|
+
if (response.ok) {
|
|
238
|
+
succeeded++;
|
|
239
|
+
} else {
|
|
240
|
+
logger.error(`Failed to delete ${docId} (HTTP ${response.status}): ${await response.text()}`);
|
|
241
|
+
failed++;
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
logger.error(`Failed to delete ${docId}: ${err}`);
|
|
245
|
+
failed++;
|
|
246
|
+
}
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return { succeeded, failed };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── Sync orchestrator ──────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
async function syncProseDocuments(opts: {
|
|
256
|
+
docsSiteId: string;
|
|
257
|
+
project: string;
|
|
258
|
+
apiKey: string;
|
|
259
|
+
pages: string[];
|
|
260
|
+
outputBasePath: string;
|
|
261
|
+
logger: AstroIntegrationLogger;
|
|
262
|
+
}) {
|
|
263
|
+
const { docsSiteId, project, apiKey, pages, outputBasePath, logger } = opts;
|
|
264
|
+
|
|
265
|
+
logger.info(bold(`Syncing ${pages.length} prose pages to docs search index`));
|
|
266
|
+
|
|
267
|
+
const localDocs = await buildLocalManifest(pages, outputBasePath);
|
|
268
|
+
const remoteDocs = await fetchRemoteManifest(docsSiteId, project, apiKey, logger);
|
|
269
|
+
const { toPut, toDelete } = diffManifests(localDocs, remoteDocs);
|
|
270
|
+
|
|
271
|
+
const unchanged = localDocs.size - toPut.length;
|
|
272
|
+
logger.info(bold(`${toPut.length} to upload, ${toDelete.length} to delete, ${unchanged} unchanged`));
|
|
273
|
+
|
|
274
|
+
if (toPut.length === 0 && toDelete.length === 0) {
|
|
275
|
+
logger.info('Docs search index is up to date');
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const uploaded =
|
|
280
|
+
toPut.length > 0
|
|
281
|
+
? await importDocuments(toPut, docsSiteId, project, apiKey, logger)
|
|
282
|
+
: { succeeded: 0, failed: 0, errors: [] as string[] };
|
|
283
|
+
|
|
284
|
+
const deleted =
|
|
285
|
+
toDelete.length > 0
|
|
286
|
+
? await deleteDocuments(toDelete, docsSiteId, project, apiKey, logger)
|
|
287
|
+
: { succeeded: 0, failed: 0 };
|
|
288
|
+
|
|
289
|
+
for (const err of uploaded.errors) {
|
|
290
|
+
logger.error(`Import error: ${err}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const failures = uploaded.failed + deleted.failed;
|
|
294
|
+
if (failures > 0) {
|
|
295
|
+
logger.error(
|
|
296
|
+
`Docs search index sync completed with ${failures} error(s): ${uploaded.succeeded} uploaded, ${deleted.succeeded} deleted`,
|
|
297
|
+
);
|
|
298
|
+
} else {
|
|
299
|
+
logger.info(
|
|
300
|
+
bold(`Docs search index synced: ${uploaded.succeeded} uploaded, ${deleted.succeeded} deleted`),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── Astro integration ──────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
export function stainlessDocsVectorProseIndexing(
|
|
308
|
+
config: NormalizedStainlessDocsConfig,
|
|
309
|
+
apiReferenceBasePath: string | null,
|
|
310
|
+
): AstroIntegration {
|
|
311
|
+
return {
|
|
312
|
+
name: 'stl-docs-prose-indexing',
|
|
313
|
+
hooks: {
|
|
314
|
+
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
315
|
+
const logger = getSharedLogger({ fallback: localLogger });
|
|
316
|
+
const outputBasePath = dir.pathname;
|
|
317
|
+
|
|
318
|
+
const project = config.apiReference?.stainlessProject;
|
|
319
|
+
const { STAINLESS_API_KEY: apiKey, STAINLESS_DOCS_SITE_ID: docsSiteId } = process.env;
|
|
320
|
+
|
|
321
|
+
if (!apiKey || !project || !docsSiteId) {
|
|
322
|
+
logger.info(
|
|
323
|
+
`Skipping vector prose search indexing: required environment/config variables not set, missing: ${[
|
|
324
|
+
!apiKey && 'STAINLESS_API_KEY',
|
|
325
|
+
!docsSiteId && 'STAINLESS_DOCS_SITE_ID',
|
|
326
|
+
!project && 'stainlessProject in apiReference config',
|
|
327
|
+
]
|
|
328
|
+
.filter(Boolean)
|
|
329
|
+
.join(', ')}`,
|
|
330
|
+
);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const pages = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
335
|
+
if (pages.length === 0) {
|
|
336
|
+
logger.info('No prose pages found to index for vector search');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
await syncProseDocuments({ docsSiteId, project, apiKey, pages, outputBasePath, logger });
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
@@ -7,29 +7,21 @@ import { bold } from '../../shared/terminalUtils';
|
|
|
7
7
|
import { getProsePages } from '../../shared/getProsePages';
|
|
8
8
|
|
|
9
9
|
export function stainlessDocsMarkdownRenderer({
|
|
10
|
-
enabled,
|
|
11
10
|
apiReferenceBasePath,
|
|
12
11
|
}: {
|
|
13
|
-
enabled: boolean;
|
|
14
12
|
apiReferenceBasePath: string | null;
|
|
15
13
|
}): AstroIntegration {
|
|
16
14
|
return {
|
|
17
15
|
name: 'stl-docs-md',
|
|
18
16
|
hooks: {
|
|
19
17
|
'astro:config:setup': ({ addMiddleware }) => {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
25
|
-
}
|
|
18
|
+
addMiddleware({
|
|
19
|
+
entrypoint: resolveSrcFile('/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts'),
|
|
20
|
+
order: 'post',
|
|
21
|
+
});
|
|
26
22
|
},
|
|
27
23
|
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
28
24
|
const logger = getSharedLogger({ fallback: localLogger });
|
|
29
|
-
if (!enabled) {
|
|
30
|
-
logger.info('Stainless Docs prose Markdown rendering is disabled, skipping...');
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
25
|
const outputBasePath = dir.pathname;
|
|
34
26
|
const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
35
27
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'astro/zod';
|
|
2
|
+
|
|
3
|
+
export const stainlessDocsSchemaExtension = z.object({
|
|
4
|
+
ogImageOptions: z
|
|
5
|
+
.object({
|
|
6
|
+
logo: z.string().optional(),
|
|
7
|
+
theme: z.enum(['light', 'dark']).default('light'),
|
|
8
|
+
title: z.string().optional(),
|
|
9
|
+
description: z.string().optional(),
|
|
10
|
+
})
|
|
11
|
+
.optional(),
|
|
12
|
+
});
|
|
@@ -102,7 +102,7 @@ export interface StarlightRouteWithStlDocs extends StarlightRouteData {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
export const onRequest = defineRouteMiddleware(
|
|
105
|
+
export const onRequest = defineRouteMiddleware((context) => {
|
|
106
106
|
// if using content collection schema, use: context.locals.starlightRoute.entry.data.stainlessStarlight
|
|
107
107
|
// this worked without collections but relied on hijacking starlightRoute: context.props.frontmatter.stainlessStarlight
|
|
108
108
|
|
package/styles/overrides.css
CHANGED
|
@@ -54,18 +54,6 @@ mobile-starlight-toc {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
display: none;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
mobile-starlight-toc {
|
|
63
|
-
nav {
|
|
64
|
-
inset-inline-start: calc(var(--sl-content-inline-start, 0));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
summary {
|
|
68
|
-
padding: 2rem 2rem;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
57
|
+
starlight-menu-button {
|
|
58
|
+
display: none;
|
|
71
59
|
}
|