@stainless-api/docs 0.1.0-beta.13 → 0.1.0-beta.130
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 +1102 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +90 -0
- package/{eslint.config.js → eslint.config.ts} +0 -2
- package/locals.d.ts +17 -0
- package/package.json +62 -44
- package/playground-virtual-modules.d.ts +96 -0
- package/plugin/assets/languages/cli.svg +14 -0
- package/plugin/assets/languages/csharp.svg +1 -0
- package/plugin/assets/languages/php.svg +4 -0
- package/plugin/buildAlgoliaIndex.ts +40 -39
- package/plugin/components/MethodDescription.tsx +54 -0
- package/plugin/components/RequestBuilder/ParamEditor.tsx +55 -0
- package/plugin/components/RequestBuilder/SnippetStainlessIsland.tsx +107 -0
- package/plugin/components/RequestBuilder/index.tsx +40 -0
- package/plugin/components/RequestBuilder/props.ts +9 -0
- package/plugin/components/RequestBuilder/spec-helpers.ts +47 -0
- package/plugin/components/RequestBuilder/styles.css +67 -0
- package/plugin/components/SDKSelect.astro +18 -111
- package/plugin/components/SnippetCode.tsx +112 -70
- package/plugin/components/StainlessIslands.tsx +126 -0
- package/plugin/components/search/SearchAlgolia.astro +46 -29
- package/plugin/components/search/SearchIsland.tsx +61 -37
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/globalJs/ai-dropdown-options.ts +248 -0
- package/plugin/globalJs/code-snippets.ts +45 -16
- package/plugin/globalJs/copy.ts +115 -27
- package/plugin/globalJs/create-playground.shim.ts +3 -0
- package/plugin/globalJs/method-descriptions.ts +33 -0
- package/plugin/globalJs/navigation.ts +24 -44
- package/plugin/globalJs/playground-data.shim.ts +1 -0
- package/plugin/globalJs/playground-data.ts +14 -0
- package/plugin/globalJs/summary-selection-tweak.ts +29 -0
- package/plugin/helpers/generateDocsRoutes.ts +59 -0
- package/plugin/helpers/multiSpec.ts +8 -0
- package/plugin/index.ts +317 -141
- package/plugin/languages.ts +8 -2
- package/plugin/loadPluginConfig.ts +284 -109
- package/plugin/markdown/highlighter.ts +100 -0
- package/plugin/markdown/index.ts +39 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +3 -1
- package/plugin/react/Routing.tsx +98 -263
- package/plugin/referencePlaceholderUtils.ts +17 -14
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +39 -35
- package/plugin/routes/Docs.astro +72 -111
- package/plugin/routes/DocsStatic.astro +6 -5
- package/plugin/routes/Overview.astro +46 -22
- package/plugin/routes/llms.ts +186 -0
- package/plugin/routes/markdown.ts +13 -12
- package/plugin/{cms → sidebar-utils}/sidebar-builder.ts +84 -69
- package/plugin/specs/FileCache.ts +99 -0
- package/plugin/specs/fetchSpecSSR.ts +27 -0
- package/plugin/specs/generateSpec.ts +112 -0
- package/plugin/specs/index.ts +132 -0
- package/plugin/specs/inputResolver.ts +148 -0
- package/plugin/{cms → specs}/worker.ts +82 -5
- package/plugin/vendor/preview.worker.docs.js +27121 -16890
- package/plugin/vendor/templates/cli.md +1 -0
- package/plugin/vendor/templates/go.md +4 -2
- package/plugin/vendor/templates/java.md +5 -1
- package/plugin/vendor/templates/kotlin.md +5 -1
- package/plugin/vendor/templates/node.md +4 -2
- package/plugin/vendor/templates/python.md +4 -2
- package/plugin/vendor/templates/ruby.md +4 -2
- package/plugin/vendor/templates/terraform.md +1 -1
- package/plugin/vendor/templates/typescript.md +3 -1
- package/resolveSrcFile.ts +10 -0
- package/scripts/vendor_deps.ts +5 -5
- package/shared/conditionalIntegration.ts +28 -0
- package/shared/getProsePages.ts +41 -0
- package/shared/getSharedLogger.ts +15 -0
- package/shared/terminalUtils.ts +3 -0
- package/shared/virtualModule.ts +46 -1
- package/src/content.config.ts +9 -0
- package/stl-docs/aiChatExamples.ts +95 -0
- package/stl-docs/chat/docs-chat-handler.ts +18 -0
- package/stl-docs/chat/hook.ts +215 -0
- package/stl-docs/chat/schemas.ts +70 -0
- package/stl-docs/chat/stainless-handler/index.ts +126 -0
- package/stl-docs/chat/stream-util.ts +16 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +188 -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 +96 -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 +109 -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/AIDropdown.tsx +63 -0
- package/stl-docs/components/AiChatIsland.tsx +16 -0
- package/stl-docs/components/{content-panel/ContentBreadcrumbs.tsx → ContentBreadcrumbs.tsx} +2 -2
- package/stl-docs/components/ContentPanel.astro +9 -0
- package/stl-docs/components/Footer.astro +89 -0
- package/stl-docs/components/Head.astro +20 -0
- package/stl-docs/components/Header.astro +3 -9
- package/stl-docs/components/PageFrame.astro +37 -0
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/PageTitle.astro +82 -0
- package/stl-docs/components/StainlessLogo.svg +4 -0
- package/stl-docs/components/ThemeProvider.astro +36 -0
- package/stl-docs/components/ThemeSelect.astro +84 -146
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/headers/DefaultHeader.astro +6 -8
- package/stl-docs/components/headers/StackedHeader.astro +10 -53
- package/stl-docs/components/icons/chat-gpt.tsx +2 -2
- package/stl-docs/components/icons/cursor.tsx +10 -0
- package/stl-docs/components/icons/gemini.tsx +19 -0
- package/stl-docs/components/icons/markdown.tsx +1 -1
- package/stl-docs/components/index.ts +1 -0
- 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 +6 -6
- 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/mintlify-compat/card.css +4 -4
- package/stl-docs/components/mintlify-compat/index.ts +2 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +38 -77
- package/stl-docs/components/nav-tabs/NavTabs.astro +81 -81
- package/stl-docs/components/nav-tabs/SecondaryNavTabs.astro +1 -2
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +5 -2
- package/stl-docs/components/pagination/HomeLink.astro +10 -0
- package/stl-docs/components/pagination/Pagination.astro +177 -0
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +22 -0
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +13 -0
- package/stl-docs/components/pagination/util.ts +71 -0
- package/stl-docs/components/scripts.ts +1 -0
- package/stl-docs/components/sidebars/BaseSidebar.astro +80 -2
- package/stl-docs/components/sidebars/SidebarWithComponents.tsx +10 -0
- package/stl-docs/components/sidebars/convertAstroSidebarToStl.tsx +62 -0
- package/stl-docs/disableCalloutSyntax.ts +36 -0
- package/stl-docs/fonts.ts +186 -0
- package/stl-docs/index.ts +176 -58
- package/stl-docs/loadStlDocsConfig.ts +73 -8
- package/stl-docs/proseDocSync.test.ts +74 -0
- package/stl-docs/proseDocSync.ts +344 -0
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +53 -0
- package/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts +41 -0
- package/stl-docs/proseMarkdown/toMarkdown.ts +158 -0
- package/stl-docs/proseSearchIndexing.ts +218 -0
- package/stl-docs/tabsMiddleware.ts +14 -5
- package/styles/code.css +53 -49
- package/styles/links.css +2 -37
- package/styles/method-descriptions.css +36 -0
- package/styles/overrides.css +28 -46
- package/styles/page.css +228 -38
- package/styles/sdk_select.css +9 -6
- package/styles/search.css +11 -21
- package/styles/sidebar.css +28 -215
- package/styles/{variables.css → sl-variables.css} +4 -8
- package/styles/stldocs-variables.css +6 -0
- package/styles/toc.css +19 -8
- package/theme.css +11 -9
- package/tsconfig.json +1 -4
- package/virtual-module.d.ts +66 -8
- package/components/variables.css +0 -112
- package/plugin/cms/client.ts +0 -62
- package/plugin/cms/server.ts +0 -268
- package/plugin/globalJs/ai-dropdown.ts +0 -57
- package/stl-docs/components/APIReferenceAIDropdown.tsx +0 -58
- package/stl-docs/components/ClientRouterHead.astro +0 -41
- package/stl-docs/components/content-panel/ContentPanel.astro +0 -69
- package/stl-docs/components/content-panel/ProseAIDropdown.tsx +0 -55
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -49
- package/stl-docs/components/mintlify-compat/Step.astro +0 -56
- package/stl-docs/components/mintlify-compat/Steps.astro +0 -15
- package/styles/fonts.css +0 -68
- /package/{plugin/assets → assets}/fonts/geist/OFL.txt +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-italic-latin.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin-ext.woff2 +0 -0
- /package/{plugin/assets → assets}/fonts/geist/geist-mono-latin.woff2 +0 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AstroIntegration } from 'astro';
|
|
2
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
3
|
+
import { toMarkdown } from './toMarkdown';
|
|
4
|
+
import { resolveSrcFile } from '../../resolveSrcFile';
|
|
5
|
+
import { getSharedLogger } from '../../shared/getSharedLogger';
|
|
6
|
+
import { bold } from '../../shared/terminalUtils';
|
|
7
|
+
import { getProsePages } from '../../shared/getProsePages';
|
|
8
|
+
|
|
9
|
+
export function stainlessDocsMarkdownRenderer({
|
|
10
|
+
apiReferenceBasePath,
|
|
11
|
+
}: {
|
|
12
|
+
apiReferenceBasePath: string | null;
|
|
13
|
+
}): AstroIntegration {
|
|
14
|
+
return {
|
|
15
|
+
name: 'stl-docs-md',
|
|
16
|
+
hooks: {
|
|
17
|
+
'astro:config:setup': ({ addMiddleware }) => {
|
|
18
|
+
addMiddleware({
|
|
19
|
+
entrypoint: resolveSrcFile('/stl-docs/proseMarkdown/proseMarkdownMiddleware.ts'),
|
|
20
|
+
order: 'post',
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
24
|
+
const logger = getSharedLogger({ fallback: localLogger });
|
|
25
|
+
const outputBasePath = dir.pathname;
|
|
26
|
+
const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
27
|
+
|
|
28
|
+
logger.info(bold(`Building ${pagesToRender.length} Markdown pages for prose content`));
|
|
29
|
+
|
|
30
|
+
let completedCount = 0;
|
|
31
|
+
|
|
32
|
+
for (const absHtmlPath of pagesToRender) {
|
|
33
|
+
const txt = await readFile(absHtmlPath, 'utf-8');
|
|
34
|
+
const md = await toMarkdown(txt);
|
|
35
|
+
if (md) {
|
|
36
|
+
const absMdPath = absHtmlPath.replace('.html', '.md');
|
|
37
|
+
await writeFile(absMdPath, md, 'utf-8');
|
|
38
|
+
|
|
39
|
+
completedCount++;
|
|
40
|
+
|
|
41
|
+
const relHtmlPath = absHtmlPath.replace(outputBasePath, '');
|
|
42
|
+
const relMdPath = absMdPath.replace(outputBasePath, '');
|
|
43
|
+
|
|
44
|
+
logger.info(`(${completedCount}/${pagesToRender.length}) ${relHtmlPath} -> ${relMdPath}`);
|
|
45
|
+
} else {
|
|
46
|
+
logger.error(`Failed to render ${absHtmlPath} as Markdown`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
2
|
+
import { toMarkdown } from './toMarkdown';
|
|
3
|
+
import { API_REFERENCE_BASE_PATH } from 'virtual:stl-docs-virtual-module';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
// this is only run in `astro dev` for rendering prose content as Markdown on the fly.
|
|
7
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
8
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
|
9
|
+
if (!import.meta.env.DEV) {
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const resolvedBasePath = path.posix.join(import.meta.env.BASE_URL ?? '', API_REFERENCE_BASE_PATH);
|
|
14
|
+
if (resolvedBasePath && context.url.pathname.startsWith(resolvedBasePath)) {
|
|
15
|
+
// handled by the API reference API route in stl-starlight plugin
|
|
16
|
+
return next();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!context.url.pathname.endsWith('/index.md')) {
|
|
20
|
+
return next();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pathname = context.url.pathname.replace('index.md', '');
|
|
24
|
+
|
|
25
|
+
// We must trim the trailing slash to support astro configs with `trailingSlash: 'never'`
|
|
26
|
+
const cleanPathname = pathname !== '/' ? pathname.replace(/\/$/, '') : pathname;
|
|
27
|
+
const htmlUrl = new URL(cleanPathname, context.url);
|
|
28
|
+
|
|
29
|
+
const resp = await fetch(htmlUrl);
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
return new Response('Failed to fetch HTML', { status: 400 });
|
|
32
|
+
}
|
|
33
|
+
const html = await resp.text();
|
|
34
|
+
const md = await toMarkdown(html);
|
|
35
|
+
|
|
36
|
+
if (!md) {
|
|
37
|
+
return new Response('Failed to render Markdown', { status: 400 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Response(md, { status: 200 });
|
|
41
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { unified } from 'unified';
|
|
2
|
+
import rehypeParse from 'rehype-parse';
|
|
3
|
+
import rehypeRemark from 'rehype-remark';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import remarkStringify from 'remark-stringify';
|
|
6
|
+
import { HTMLElement, parse } from 'node-html-parser';
|
|
7
|
+
|
|
8
|
+
type PaginationLink = {
|
|
9
|
+
href: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PaginationItems = {
|
|
14
|
+
prev: PaginationLink | null;
|
|
15
|
+
next: PaginationLink | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function parsePaginationLink(footer: HTMLElement, rel: 'next' | 'prev'): PaginationLink | null {
|
|
19
|
+
const link = footer.querySelector(`.pagination-links a[rel="${rel}"]`);
|
|
20
|
+
if (!link) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const title = link.querySelector('.link-title');
|
|
25
|
+
if (!title) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const href = link.getAttribute('href');
|
|
30
|
+
if (!href) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
href,
|
|
36
|
+
label: title.text,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRelativeLink(href: string) {
|
|
41
|
+
return href.startsWith('/');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasExtension(href: string) {
|
|
45
|
+
return href.includes('.');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function removeTrailingSlash(href: string) {
|
|
49
|
+
return href.replace(/\/$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeMarkdownLinks(el: HTMLElement) {
|
|
53
|
+
el.querySelectorAll('a').forEach((a) => {
|
|
54
|
+
const href = a.getAttribute('href');
|
|
55
|
+
if (!href) {
|
|
56
|
+
return a;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (isRelativeLink(href) && !hasExtension(href)) {
|
|
60
|
+
if (href === '/') {
|
|
61
|
+
a.setAttribute('href', '/index.md');
|
|
62
|
+
} else {
|
|
63
|
+
a.setAttribute('href', `${removeTrailingSlash(href)}/index.md`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return a;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function removeHiddenElements(el: HTMLElement) {
|
|
71
|
+
const hiddenSelectors = ['.sl-anchor-link'];
|
|
72
|
+
for (const selector of hiddenSelectors) {
|
|
73
|
+
const hiddenElements = el.querySelectorAll(selector);
|
|
74
|
+
for (const hiddenElement of hiddenElements) {
|
|
75
|
+
hiddenElement.remove();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function toMarkdown(html: string) {
|
|
81
|
+
const root = parse(html);
|
|
82
|
+
|
|
83
|
+
const mainEl = root.querySelector('main');
|
|
84
|
+
|
|
85
|
+
if (!mainEl) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
makeMarkdownLinks(mainEl);
|
|
90
|
+
|
|
91
|
+
const footer = mainEl.querySelector('footer');
|
|
92
|
+
|
|
93
|
+
const markdownContentEl = mainEl.querySelector('.sl-markdown-content');
|
|
94
|
+
if (!markdownContentEl) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
removeHiddenElements(markdownContentEl);
|
|
99
|
+
|
|
100
|
+
const articleContent = markdownContentEl.innerHTML;
|
|
101
|
+
|
|
102
|
+
const paginationLinks: PaginationItems = {
|
|
103
|
+
prev: null,
|
|
104
|
+
next: null,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (footer) {
|
|
108
|
+
paginationLinks.prev = parsePaginationLink(footer, 'prev');
|
|
109
|
+
paginationLinks.next = parsePaginationLink(footer, 'next');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let md = (
|
|
113
|
+
await unified()
|
|
114
|
+
.use(rehypeParse, { fragment: true }) // parse HTML
|
|
115
|
+
.use(rehypeRemark) // rehype (HTML) -> remark (MD AST)
|
|
116
|
+
.use(remarkGfm) // tables, strikethrough, autolinks, etc.
|
|
117
|
+
.use(remarkStringify, {
|
|
118
|
+
fences: true,
|
|
119
|
+
bullet: '-',
|
|
120
|
+
listItemIndent: 'one',
|
|
121
|
+
rule: '-',
|
|
122
|
+
})
|
|
123
|
+
.process(articleContent)
|
|
124
|
+
).toString();
|
|
125
|
+
|
|
126
|
+
const title = root.querySelector('title')?.textContent;
|
|
127
|
+
const description = root.querySelector('meta[name="description"]')?.attributes.content;
|
|
128
|
+
const lastUpdated = root.querySelector('.meta time')?.attributes.datetime;
|
|
129
|
+
|
|
130
|
+
// let htmlUrl = url.toString().replace('.md', '');
|
|
131
|
+
// if (htmlUrl.endsWith('/index')) {
|
|
132
|
+
// htmlUrl = htmlUrl.replace('/index', '');
|
|
133
|
+
// }
|
|
134
|
+
|
|
135
|
+
md = [
|
|
136
|
+
'---',
|
|
137
|
+
`title: ${title}`,
|
|
138
|
+
description ? `description: ${description}` : [],
|
|
139
|
+
lastUpdated ? `lastUpdated: ${lastUpdated}` : [],
|
|
140
|
+
// `source_url:`,
|
|
141
|
+
// ` html: ${htmlUrl}`,
|
|
142
|
+
// ` md: ${url.toString()}`,
|
|
143
|
+
'---\n',
|
|
144
|
+
md,
|
|
145
|
+
]
|
|
146
|
+
.flat()
|
|
147
|
+
.join('\n');
|
|
148
|
+
|
|
149
|
+
if (paginationLinks.prev) {
|
|
150
|
+
md += `\n\n[Previous](${paginationLinks.prev.href})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (paginationLinks.next) {
|
|
154
|
+
md += `\n\n[Next](${paginationLinks.next.href})`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return md;
|
|
158
|
+
}
|