@stainless-api/docs 0.1.0-beta.103 → 0.1.0-beta.105
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 +21 -0
- package/package.json +5 -5
- package/plugin/components/SDKSelect.astro +0 -6
- package/plugin/components/search/SearchAlgolia.astro +1 -1
- package/plugin/routes/Overview.astro +2 -1
- package/plugin/vendor/preview.worker.docs.js +12647 -8256
- package/plugin/vendor/templates/java.md +2 -0
- package/plugin/vendor/templates/kotlin.md +2 -0
- package/stl-docs/components/Header.astro +0 -5
- package/stl-docs/components/PageFrame.astro +21 -5
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/content-panel/ContentPanel.astro +2 -35
- package/stl-docs/components/headers/DefaultHeader.astro +4 -6
- package/stl-docs/components/headers/StackedHeader.astro +3 -51
- package/stl-docs/components/nav-tabs/NavTabs.astro +2 -2
- package/stl-docs/index.ts +4 -1
- package/stl-docs/proseDocSync.ts +314 -0
- package/stl-docs/proseSearchIndexing.ts +3 -387
- package/styles/overrides.css +2 -14
- package/styles/page.css +216 -69
- package/styles/sidebar.css +27 -17
- package/styles/sl-variables.css +3 -8
- package/styles/stldocs-variables.css +2 -2
- package/styles/toc.css +8 -0
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -65
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
2
|
+
import Sidebar from 'virtual:starlight/components/Sidebar';
|
|
3
|
+
import MobileMenuToggle from 'virtual:starlight/components/MobileMenuToggle';
|
|
4
|
+
|
|
3
5
|
import AiChat from 'virtual:stl-docs/components/AiChat.tsx'; // conditionally resolves to null if ai chat module is not injected
|
|
4
6
|
import AiChatIsland from './AiChatIsland.tsx'; // entrypoint for client island can’t be a virtual module
|
|
5
7
|
|
|
6
8
|
import starlightConfig from 'virtual:starlight/user-config';
|
|
7
9
|
const locale = Astro.currentLocale ?? starlightConfig.defaultLocale.lang;
|
|
8
10
|
const siteTitle = locale && starlightConfig.title[locale];
|
|
11
|
+
|
|
12
|
+
const { hasSidebar } = Astro.locals.starlightRoute;
|
|
13
|
+
import clsx from 'clsx';
|
|
9
14
|
---
|
|
10
15
|
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
16
|
+
<div class="page">
|
|
17
|
+
<header>
|
|
18
|
+
<slot name="header" slot="header" />
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<nav
|
|
22
|
+
class={clsx('sidebar', !hasSidebar && 'hidden')}
|
|
23
|
+
aria-label={Astro.locals.t('sidebarNav.accessibleLabel')}
|
|
24
|
+
>
|
|
25
|
+
<MobileMenuToggle />
|
|
26
|
+
<div id="starlight__sidebar" class="sidebar-pane">
|
|
27
|
+
<Sidebar />
|
|
28
|
+
</div>
|
|
29
|
+
</nav>
|
|
14
30
|
|
|
15
31
|
<slot />
|
|
16
32
|
|
|
17
33
|
{!!AiChat && <AiChatIsland client:load currentLanguage={Astro.locals.language} siteTitle={siteTitle} />}
|
|
18
|
-
</
|
|
34
|
+
</div>
|
|
@@ -1,42 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
<div class="content-panel sl-container stl-ui-prose">
|
|
1
|
+
<div class="stl-ui-prose stl-content-panel">
|
|
6
2
|
<slot />
|
|
7
3
|
</div>
|
|
8
4
|
|
|
9
5
|
<style>
|
|
10
|
-
|
|
11
|
-
.content-panel {
|
|
12
|
-
padding: 1.5rem var(--sl-content-pad-x);
|
|
13
|
-
}
|
|
14
|
-
.sl-container {
|
|
15
|
-
max-width: var(--sl-content-width);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
@media (min-width: 72rem) {
|
|
19
|
-
.sl-container {
|
|
20
|
-
margin-inline: var(--sl-content-margin-inline, auto);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.content-panel {
|
|
26
|
-
padding: 0 var(--sl-content-pad-x) 0 var(--sl-content-pad-x);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.content-panel + .content-panel {
|
|
6
|
+
.stl-content-panel + .stl-content-panel {
|
|
30
7
|
margin-top: 24px;
|
|
31
8
|
}
|
|
32
|
-
|
|
33
|
-
@media (min-width: 50rem) {
|
|
34
|
-
.content-panel {
|
|
35
|
-
padding: 0 0 0 var(--sl-content-pad-x);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.stl-prose-page-nav-container {
|
|
40
|
-
padding: 1rem 0 0;
|
|
41
|
-
}
|
|
42
9
|
</style>
|
|
@@ -6,17 +6,15 @@ import HeaderLinks from './HeaderLinks.astro';
|
|
|
6
6
|
import NavLinks from '../nav-tabs/NavTabs.astro';
|
|
7
7
|
import { TABS } from 'virtual:stl-docs-virtual-module';
|
|
8
8
|
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
|
|
9
|
-
import SplashMobileMenuToggle from './SplashMobileMenuToggle.astro';
|
|
10
9
|
|
|
11
10
|
interface Props {
|
|
12
11
|
shouldRenderSearch?: boolean;
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
const { shouldRenderSearch } = Astro.props;
|
|
16
|
-
const { hasSidebar } = Astro.locals.starlightRoute;
|
|
17
15
|
---
|
|
18
16
|
|
|
19
|
-
<div class="header
|
|
17
|
+
<div class="header">
|
|
20
18
|
<div class="left-group">
|
|
21
19
|
<div class="title-wrapper sl-flex">
|
|
22
20
|
<SiteTitle />
|
|
@@ -24,9 +22,9 @@ const { hasSidebar } = Astro.locals.starlightRoute;
|
|
|
24
22
|
</div>
|
|
25
23
|
{TABS.length > 0 && <NavLinks />}
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
{shouldRenderSearch && <Search />}
|
|
26
|
+
|
|
27
|
+
<div class="right-group">
|
|
30
28
|
<div class="sl-hidden md:sl-flex">
|
|
31
29
|
<ThemeSelect />
|
|
32
30
|
<HeaderLinks />
|
|
@@ -3,32 +3,26 @@ import LanguageSelect from 'virtual:starlight/components/LanguageSelect';
|
|
|
3
3
|
import Search from 'virtual:starlight/components/Search';
|
|
4
4
|
import SiteTitle from 'virtual:starlight/components/SiteTitle';
|
|
5
5
|
import HeaderLinks from './HeaderLinks.astro';
|
|
6
|
-
import { TABS } from 'virtual:stl-docs-virtual-module';
|
|
7
6
|
import ThemeSelect from 'virtual:starlight/components/ThemeSelect';
|
|
8
7
|
import SecondaryNavTabs from '../nav-tabs/SecondaryNavTabs.astro';
|
|
9
|
-
import SplashMobileMenuToggle from './SplashMobileMenuToggle.astro';
|
|
10
8
|
|
|
11
9
|
interface Props {
|
|
12
10
|
shouldRenderSearch?: boolean;
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
const { shouldRenderSearch } = Astro.props;
|
|
16
|
-
const { hasSidebar } = Astro.locals.starlightRoute;
|
|
17
14
|
---
|
|
18
15
|
|
|
19
|
-
<div class="header
|
|
16
|
+
<div class="header">
|
|
20
17
|
<div class="left-group">
|
|
21
18
|
<div class="title-wrapper sl-flex">
|
|
22
19
|
<SiteTitle />
|
|
23
20
|
</div>
|
|
24
21
|
</div>
|
|
25
22
|
|
|
26
|
-
<
|
|
27
|
-
{shouldRenderSearch && <Search />}
|
|
28
|
-
{!hasSidebar && <SplashMobileMenuToggle />}
|
|
29
|
-
</div>
|
|
23
|
+
{shouldRenderSearch && <Search />}
|
|
30
24
|
|
|
31
|
-
<div class=
|
|
25
|
+
<div class="right-group">
|
|
32
26
|
<ThemeSelect />
|
|
33
27
|
<HeaderLinks />
|
|
34
28
|
<LanguageSelect />
|
|
@@ -36,45 +30,3 @@ const { hasSidebar } = Astro.locals.starlightRoute;
|
|
|
36
30
|
</div>
|
|
37
31
|
|
|
38
32
|
<SecondaryNavTabs />
|
|
39
|
-
|
|
40
|
-
<style is:inline>
|
|
41
|
-
.stl-top-container {
|
|
42
|
-
display: flex;
|
|
43
|
-
flex-direction: row;
|
|
44
|
-
align-items: center;
|
|
45
|
-
justify-content: end;
|
|
46
|
-
gap: 0.5rem;
|
|
47
|
-
width: 100%;
|
|
48
|
-
}
|
|
49
|
-
@media (min-width: 50rem) {
|
|
50
|
-
:root {
|
|
51
|
-
--sl-nav-height: 88px;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
header.header {
|
|
55
|
-
display: flex;
|
|
56
|
-
flex-direction: column;
|
|
57
|
-
gap: 0.29rem;
|
|
58
|
-
padding-bottom: 0;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.stl-top-container {
|
|
62
|
-
justify-content: center;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
</style>
|
|
66
|
-
|
|
67
|
-
{
|
|
68
|
-
TABS.length === 0 && (
|
|
69
|
-
<style
|
|
70
|
-
is:inline
|
|
71
|
-
set:text={`
|
|
72
|
-
@media (min-width: 50rem) {
|
|
73
|
-
:root {
|
|
74
|
-
--sl-nav-height: 56px;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
`}
|
|
78
|
-
/>
|
|
79
|
-
)
|
|
80
|
-
}
|
|
@@ -90,8 +90,8 @@ const navLinks = buildNavLinks(Astro.locals.starlightRoute);
|
|
|
90
90
|
|
|
91
91
|
localStorage.setItem('stl-nav-links-mode', mode);
|
|
92
92
|
|
|
93
|
-
document.documentElement.classList.
|
|
94
|
-
document.documentElement.classList.
|
|
93
|
+
document.documentElement.classList.toggle('stl-nav-links-mode-desktop', mode === 'desktop');
|
|
94
|
+
document.documentElement.classList.toggle('stl-nav-links-mode-mobile', mode === 'mobile');
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
const resizeObserver = new ResizeObserver(() => {
|
package/stl-docs/index.ts
CHANGED
|
@@ -21,7 +21,8 @@ import type * as StlDocsVirtualModule from 'virtual:stl-docs-virtual-module';
|
|
|
21
21
|
import { resolveSrcFile } from '../resolveSrcFile';
|
|
22
22
|
import { stainlessDocsMarkdownRenderer } from './proseMarkdown/proseMarkdownIntegration';
|
|
23
23
|
import { setSharedLogger } from '../shared/getSharedLogger';
|
|
24
|
-
import {
|
|
24
|
+
import { stainlessDocsVectorProseIndexing } from './proseDocSync';
|
|
25
|
+
import { stainlessDocsAlgoliaProseIndexing } from './proseSearchIndexing';
|
|
25
26
|
import { stainlessStarlight } from '../plugin';
|
|
26
27
|
import { getFontRoles, flattenFonts } from './fonts';
|
|
27
28
|
|
|
@@ -50,6 +51,7 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
|
|
|
50
51
|
type ComponentOverrides = StarlightConfigDefined['components'];
|
|
51
52
|
const componentOverrides: ComponentOverrides = {
|
|
52
53
|
PageFrame: resolveSrcFile(COMPONENTS_FOLDER, './PageFrame.astro'),
|
|
54
|
+
TwoColumnContent: resolveSrcFile(COMPONENTS_FOLDER, './TwoColumnContent.astro'),
|
|
53
55
|
|
|
54
56
|
Head: resolveSrcFile(COMPONENTS_FOLDER, './Head.astro'),
|
|
55
57
|
Header: resolveSrcFile(COMPONENTS_FOLDER, './Header.astro'),
|
|
@@ -58,6 +60,7 @@ function stainlessDocsStarlightIntegration(config: NormalizedStainlessDocsConfig
|
|
|
58
60
|
|
|
59
61
|
Sidebar: resolveSrcFile(COMPONENTS_FOLDER, './sidebars/BaseSidebar.astro'),
|
|
60
62
|
ContentPanel: resolveSrcFile(COMPONENTS_FOLDER, './content-panel/ContentPanel.astro'),
|
|
63
|
+
PageSidebar: resolveSrcFile(COMPONENTS_FOLDER, './PageSidebar.astro'),
|
|
61
64
|
TableOfContents: resolveSrcFile(COMPONENTS_FOLDER, './TableOfContents.astro'),
|
|
62
65
|
|
|
63
66
|
PageTitle: resolveSrcFile(COMPONENTS_FOLDER, './PageTitle.astro'),
|
|
@@ -0,0 +1,314 @@
|
|
|
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
|
+
// ─── API client ──────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
async function docsApiRequest(
|
|
15
|
+
method: string,
|
|
16
|
+
apiPath: string,
|
|
17
|
+
apiKey: string,
|
|
18
|
+
body?: object,
|
|
19
|
+
): Promise<Response> {
|
|
20
|
+
const headers: Record<string, string> = {
|
|
21
|
+
Authorization: `Bearer ${apiKey}`,
|
|
22
|
+
};
|
|
23
|
+
if (body) {
|
|
24
|
+
headers['Content-Type'] = 'application/json';
|
|
25
|
+
}
|
|
26
|
+
return fetch(`${DOCS_API_BASE_URL}${apiPath}`, {
|
|
27
|
+
method,
|
|
28
|
+
headers,
|
|
29
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Manifest & diffing ─────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
type LocalDoc = { content: Buffer; sha256: string; source: string };
|
|
36
|
+
|
|
37
|
+
function sha256(content: Buffer): string {
|
|
38
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function buildLocalManifest(pages: string[], outputBasePath: string): Promise<Map<string, LocalDoc>> {
|
|
42
|
+
const docs = new Map<string, LocalDoc>();
|
|
43
|
+
for (const absHtmlPath of pages) {
|
|
44
|
+
const content = await readFile(absHtmlPath);
|
|
45
|
+
const docId = path.relative(outputBasePath, absHtmlPath);
|
|
46
|
+
docs.set(docId, { content, sha256: sha256(content), source: '/' + docId });
|
|
47
|
+
}
|
|
48
|
+
return docs;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchRemoteManifest(
|
|
52
|
+
docsSiteId: string,
|
|
53
|
+
project: string,
|
|
54
|
+
apiKey: string,
|
|
55
|
+
logger: AstroIntegrationLogger,
|
|
56
|
+
): Promise<Map<string, string>> {
|
|
57
|
+
try {
|
|
58
|
+
const response = await docsApiRequest(
|
|
59
|
+
'GET',
|
|
60
|
+
`/api/docs-sites/${docsSiteId}/documents?project=${encodeURIComponent(project)}`,
|
|
61
|
+
apiKey,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
if (response.ok) {
|
|
65
|
+
const data = (await response.json()) as {
|
|
66
|
+
documents: { id: string; content_sha256: string }[];
|
|
67
|
+
};
|
|
68
|
+
return new Map(data.documents.map((d) => [d.id, d.content_sha256]));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.error(`Failed to list remote documents (HTTP ${response.status}): ${await response.text()}`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error(`Failed to list remote documents: ${err}`);
|
|
74
|
+
}
|
|
75
|
+
return new Map();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function diffManifests(
|
|
79
|
+
localDocs: Map<string, LocalDoc>,
|
|
80
|
+
remoteDocs: Map<string, string>,
|
|
81
|
+
): { toPut: (LocalDoc & { docId: string })[]; toDelete: string[] } {
|
|
82
|
+
const toPut: (LocalDoc & { docId: string })[] = [];
|
|
83
|
+
for (const [docId, local] of localDocs) {
|
|
84
|
+
if (remoteDocs.get(docId) !== local.sha256) {
|
|
85
|
+
toPut.push({ docId, ...local });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const toDelete: string[] = [];
|
|
90
|
+
for (const remoteDocId of remoteDocs.keys()) {
|
|
91
|
+
if (!localDocs.has(remoteDocId)) {
|
|
92
|
+
toDelete.push(remoteDocId);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { toPut, toDelete };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Import & delete ────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
type ImportJobResult = { succeeded: number; failed: number; errors: string[] };
|
|
102
|
+
|
|
103
|
+
async function importDocuments(
|
|
104
|
+
docs: (LocalDoc & { docId: string })[],
|
|
105
|
+
docsSiteId: string,
|
|
106
|
+
project: string,
|
|
107
|
+
apiKey: string,
|
|
108
|
+
logger: AstroIntegrationLogger,
|
|
109
|
+
): Promise<ImportJobResult> {
|
|
110
|
+
const totals: ImportJobResult = { succeeded: 0, failed: 0, errors: [] };
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < docs.length; i += 100) {
|
|
113
|
+
const batch = docs.slice(i, i + 100);
|
|
114
|
+
|
|
115
|
+
let response: Response;
|
|
116
|
+
try {
|
|
117
|
+
response = await docsApiRequest('POST', `/api/docs-sites/${docsSiteId}/documents/import`, apiKey, {
|
|
118
|
+
project,
|
|
119
|
+
documents: batch.map(({ docId, content, source }) => ({
|
|
120
|
+
id: docId,
|
|
121
|
+
content: content.toString('utf-8'),
|
|
122
|
+
content_type: 'text/html',
|
|
123
|
+
source,
|
|
124
|
+
})),
|
|
125
|
+
});
|
|
126
|
+
} catch (err) {
|
|
127
|
+
logger.error(`Failed to submit import batch: ${err}`);
|
|
128
|
+
totals.failed += batch.length;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
logger.error(`Failed to submit import batch (HTTP ${response.status}): ${await response.text()}`);
|
|
134
|
+
totals.failed += batch.length;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { job_id } = (await response.json()) as { job_id: string };
|
|
139
|
+
const result = await pollImportJob(docsSiteId, job_id, apiKey, logger);
|
|
140
|
+
totals.succeeded += result.succeeded;
|
|
141
|
+
totals.failed += result.failed;
|
|
142
|
+
totals.errors.push(...result.errors);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return totals;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function pollImportJob(
|
|
149
|
+
docsSiteId: string,
|
|
150
|
+
jobId: string,
|
|
151
|
+
apiKey: string,
|
|
152
|
+
logger: AstroIntegrationLogger,
|
|
153
|
+
): Promise<ImportJobResult> {
|
|
154
|
+
const maxWait = 5 * 60_000;
|
|
155
|
+
const start = Date.now();
|
|
156
|
+
|
|
157
|
+
while (Date.now() - start < maxWait) {
|
|
158
|
+
await new Promise((r) => setTimeout(r, 2_000));
|
|
159
|
+
|
|
160
|
+
let response: Response;
|
|
161
|
+
try {
|
|
162
|
+
response = await docsApiRequest('GET', `/api/docs-sites/${docsSiteId}/documents/jobs/${jobId}`, apiKey);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logger.error(`Failed to poll import job ${jobId}: ${err}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
logger.error(`Failed to poll import job ${jobId} (HTTP ${response.status}): ${await response.text()}`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const job = (await response.json()) as {
|
|
174
|
+
status: string;
|
|
175
|
+
succeeded: number;
|
|
176
|
+
failed: number;
|
|
177
|
+
errors: string[] | null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (job.status === 'queued' || job.status === 'processing') continue;
|
|
181
|
+
|
|
182
|
+
return { succeeded: job.succeeded, failed: job.failed, errors: job.errors ?? [] };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
logger.error(`Import job ${jobId} timed out after ${maxWait / 1000}s`);
|
|
186
|
+
return { succeeded: 0, failed: 0, errors: [`Job ${jobId} timed out`] };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function deleteDocuments(
|
|
190
|
+
docIds: string[],
|
|
191
|
+
docsSiteId: string,
|
|
192
|
+
project: string,
|
|
193
|
+
apiKey: string,
|
|
194
|
+
logger: AstroIntegrationLogger,
|
|
195
|
+
): Promise<{ succeeded: number; failed: number }> {
|
|
196
|
+
let succeeded = 0;
|
|
197
|
+
let failed = 0;
|
|
198
|
+
|
|
199
|
+
await Promise.all(
|
|
200
|
+
docIds.map(async (docId) => {
|
|
201
|
+
try {
|
|
202
|
+
const response = await docsApiRequest(
|
|
203
|
+
'DELETE',
|
|
204
|
+
`/api/docs-sites/${docsSiteId}/documents/${encodeURIComponent(docId)}?project=${encodeURIComponent(project)}`,
|
|
205
|
+
apiKey,
|
|
206
|
+
);
|
|
207
|
+
if (response.ok) {
|
|
208
|
+
succeeded++;
|
|
209
|
+
} else {
|
|
210
|
+
logger.error(`Failed to delete ${docId} (HTTP ${response.status}): ${await response.text()}`);
|
|
211
|
+
failed++;
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
logger.error(`Failed to delete ${docId}: ${err}`);
|
|
215
|
+
failed++;
|
|
216
|
+
}
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return { succeeded, failed };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ─── Sync orchestrator ──────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async function syncProseDocuments(opts: {
|
|
226
|
+
docsSiteId: string;
|
|
227
|
+
project: string;
|
|
228
|
+
apiKey: string;
|
|
229
|
+
pages: string[];
|
|
230
|
+
outputBasePath: string;
|
|
231
|
+
logger: AstroIntegrationLogger;
|
|
232
|
+
}) {
|
|
233
|
+
const { docsSiteId, project, apiKey, pages, outputBasePath, logger } = opts;
|
|
234
|
+
|
|
235
|
+
logger.info(bold(`Syncing ${pages.length} prose pages to docs search index`));
|
|
236
|
+
|
|
237
|
+
const localDocs = await buildLocalManifest(pages, outputBasePath);
|
|
238
|
+
const remoteDocs = await fetchRemoteManifest(docsSiteId, project, apiKey, logger);
|
|
239
|
+
const { toPut, toDelete } = diffManifests(localDocs, remoteDocs);
|
|
240
|
+
|
|
241
|
+
const unchanged = localDocs.size - toPut.length;
|
|
242
|
+
logger.info(bold(`${toPut.length} to upload, ${toDelete.length} to delete, ${unchanged} unchanged`));
|
|
243
|
+
|
|
244
|
+
if (toPut.length === 0 && toDelete.length === 0) {
|
|
245
|
+
logger.info('Docs search index is up to date');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const uploaded =
|
|
250
|
+
toPut.length > 0
|
|
251
|
+
? await importDocuments(toPut, docsSiteId, project, apiKey, logger)
|
|
252
|
+
: { succeeded: 0, failed: 0, errors: [] as string[] };
|
|
253
|
+
|
|
254
|
+
const deleted =
|
|
255
|
+
toDelete.length > 0
|
|
256
|
+
? await deleteDocuments(toDelete, docsSiteId, project, apiKey, logger)
|
|
257
|
+
: { succeeded: 0, failed: 0 };
|
|
258
|
+
|
|
259
|
+
for (const err of uploaded.errors) {
|
|
260
|
+
logger.error(`Import error: ${err}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const failures = uploaded.failed + deleted.failed;
|
|
264
|
+
if (failures > 0) {
|
|
265
|
+
logger.error(
|
|
266
|
+
`Docs search index sync completed with ${failures} error(s): ${uploaded.succeeded} uploaded, ${deleted.succeeded} deleted`,
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
logger.info(
|
|
270
|
+
bold(`Docs search index synced: ${uploaded.succeeded} uploaded, ${deleted.succeeded} deleted`),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ─── Astro integration ──────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
export function stainlessDocsVectorProseIndexing(
|
|
278
|
+
config: NormalizedStainlessDocsConfig,
|
|
279
|
+
apiReferenceBasePath: string | null,
|
|
280
|
+
): AstroIntegration {
|
|
281
|
+
return {
|
|
282
|
+
name: 'stl-docs-prose-indexing',
|
|
283
|
+
hooks: {
|
|
284
|
+
'astro:build:done': async ({ logger: localLogger, dir }) => {
|
|
285
|
+
const logger = getSharedLogger({ fallback: localLogger });
|
|
286
|
+
const outputBasePath = dir.pathname;
|
|
287
|
+
|
|
288
|
+
const project = config.apiReference?.stainlessProject;
|
|
289
|
+
const { STAINLESS_API_KEY: apiKey, STAINLESS_DOCS_SITE_ID: docsSiteId } = process.env;
|
|
290
|
+
|
|
291
|
+
if (!apiKey || !project || !docsSiteId) {
|
|
292
|
+
logger.info(
|
|
293
|
+
`Skipping vector prose search indexing: required environment/config variables not set, missing: ${[
|
|
294
|
+
!apiKey && 'STAINLESS_API_KEY',
|
|
295
|
+
!docsSiteId && 'STAINLESS_DOCS_SITE_ID',
|
|
296
|
+
!project && 'stainlessProject in apiReference config',
|
|
297
|
+
]
|
|
298
|
+
.filter(Boolean)
|
|
299
|
+
.join(', ')}`,
|
|
300
|
+
);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const pages = await getProsePages({ apiReferenceBasePath, outputBasePath });
|
|
305
|
+
if (pages.length === 0) {
|
|
306
|
+
logger.info('No prose pages found to index for vector search');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await syncProseDocuments({ docsSiteId, project, apiKey, pages, outputBasePath, logger });
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|