@stainless-api/docs 0.1.0-beta.126 → 0.1.0-beta.127

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,16 @@
1
1
  # @stainless-api/docs
2
2
 
3
+ ## 0.1.0-beta.127
4
+
5
+ ### Patch Changes
6
+
7
+ - e0d595f: Fix snippet copy button click handler
8
+ - 8752b4e: Fix getProsePages treating all pages as prose when API reference basePath is ""
9
+ - Updated dependencies [871f3c0]
10
+ - Updated dependencies [871f3c0]
11
+ - @stainless-api/docs-ui@0.1.0-beta.91
12
+ - @stainless-api/docs-search@0.1.0-beta.44
13
+
3
14
  ## 0.1.0-beta.126
4
15
 
5
16
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stainless-api/docs",
3
- "version": "0.1.0-beta.126",
3
+ "version": "0.1.0-beta.127",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -43,12 +43,12 @@
43
43
  "dependencies": {
44
44
  "@astrojs/markdown-remark": "^7.1.0",
45
45
  "@astrojs/react": "^5.0.2",
46
- "@markdoc/markdoc": "^0.5.6",
46
+ "@markdoc/markdoc": "^0.5.7",
47
47
  "@stainless-api/sdk": "0.5.0",
48
48
  "astro-expressive-code": "^0.41.7",
49
49
  "cheerio": "^1.2.0",
50
50
  "clsx": "^2.1.1",
51
- "dotenv": "17.3.1",
51
+ "dotenv": "17.4.0",
52
52
  "lucide-react": "^0.577.0",
53
53
  "node-html-parser": "^7.1.0",
54
54
  "rehype-parse": "^9.0.1",
@@ -61,8 +61,8 @@
61
61
  "vite-plugin-prebundle-workers": "^0.2.0",
62
62
  "web-worker": "^1.5.0",
63
63
  "yaml": "^2.8.3",
64
- "@stainless-api/docs-search": "0.1.0-beta.43",
65
- "@stainless-api/docs-ui": "0.1.0-beta.90",
64
+ "@stainless-api/docs-search": "0.1.0-beta.44",
65
+ "@stainless-api/docs-ui": "0.1.0-beta.91",
66
66
  "@stainless-api/ui-primitives": "0.1.0-beta.51"
67
67
  },
68
68
  "devDependencies": {
@@ -73,7 +73,7 @@
73
73
  "react": "^19.2.4",
74
74
  "react-dom": "^19.2.4",
75
75
  "tsx": "^4.21.0",
76
- "typescript": "5.9.3",
76
+ "typescript": "6.0.2",
77
77
  "vite": "^7.3.1",
78
78
  "vitest": "^4.1.2",
79
79
  "zod": "^4.3.6",
@@ -22,7 +22,7 @@ export function RequestBuilder({
22
22
  if (!spec) throw new Error('Spec is required for RequestBuilder');
23
23
  params = spec && extractParams(spec, method);
24
24
  } catch (e) {
25
- console.warn(e); // eslint-disable-line @eslint-react/purity
25
+ console.warn(e);
26
26
  return <div className={className}>{children}</div>;
27
27
  }
28
28
  /* eslint-enable */
@@ -33,7 +33,7 @@ const preloadPlayground = (event: Event) => {
33
33
  };
34
34
  addEventListener('mouseover', preloadPlayground);
35
35
  addEventListener('click', (event) => {
36
- if (!(event.target instanceof HTMLElement)) return;
36
+ if (!(event.target instanceof Element)) return;
37
37
  const copyButton = event.target.closest('[data-stldocs-snippet-copy]');
38
38
  if (!(copyButton instanceof HTMLElement)) return;
39
39
 
@@ -39,24 +39,22 @@ document.addEventListener(getPageLoadEvent(), () => {
39
39
  });
40
40
 
41
41
  document.addEventListener('click', (event) => {
42
- const toggle = (event.target as HTMLElement).closest(
43
- '[data-stldocs-property-toggle-expanded] > .stldocs-expand-toggle-content',
44
- )?.parentElement;
42
+ const toggle =
43
+ event.target instanceof HTMLElement && event.target.closest('[data-stldocs-property-toggle-expanded]');
44
+ if (!toggle || !(toggle instanceof HTMLElement)) return;
45
45
 
46
- if (!toggle) return;
47
-
48
- const state = toggle.dataset.stldocsPropertyToggleExpanded === 'true';
49
- toggle.dataset.stldocsPropertyToggleExpanded = state ? 'false' : 'true';
46
+ // Toggle the “expanded” state of the toggle
47
+ const toggleIsExpanded = toggle.dataset.stldocsPropertyToggleExpanded === 'true';
48
+ toggle.dataset.stldocsPropertyToggleExpanded = (!toggleIsExpanded).toString();
50
49
 
50
+ // Find the described “property group”
51
51
  const targetGroup = toggle.dataset.stldocsPropertyToggleTarget;
52
52
  if (!targetGroup) return;
53
-
54
53
  const target = document.querySelector(`[data-stldocs-property-group=${targetGroup}]`);
55
54
  if (!target) return;
56
55
 
57
- target.querySelectorAll('details').forEach((details) => {
58
- const el = details;
59
- const initial = el.dataset.stldocsExpanderInitialState;
60
- el.open = initial === 'true' ? true : !state;
56
+ // Expand or collapse all <details> elements within the target group
57
+ [...target.getElementsByTagName('details')].forEach((el) => {
58
+ el.open = el.dataset.stldocsExpanderInitialState === 'true' ? true : !toggleIsExpanded;
61
59
  });
62
60
  });
package/plugin/index.ts CHANGED
@@ -30,7 +30,7 @@ import prebundleWorkers from 'vite-plugin-prebundle-workers';
30
30
  import { SpecLoader, startSpecLoader } from './specs';
31
31
 
32
32
  import type * as ReferenceSidebarsVirtualModule from 'virtual:stl-starlight-reference-sidebars';
33
- import { generateMissingRouteList } from '@stainless-api/docs-ui/routing';
33
+ import { generateMissingRouteList, generateRouteList } from '@stainless-api/docs-ui/routing';
34
34
  import { buildAlgoliaIndex } from './buildAlgoliaIndex';
35
35
 
36
36
  export { generateAPILink } from './generateAPIReferenceLink';
@@ -357,13 +357,26 @@ function stlStarlightAstroIntegration(pluginConfig: NormalizedStainlessStarlight
357
357
  collectedErrors = null;
358
358
  }
359
359
 
360
+ const specComposite = await resolveSpecs();
361
+
362
+ // TODO: (multi-spec) support multiple specs
363
+ const spec = specComposite.listUniqueSpecs()[0]!.data.sdkJson;
364
+
365
+ const basePrefixInOutput = path.posix.join(astroBase, pluginConfig.basePath).replace(/^\/+/, '');
366
+ const routeList = generateRouteList({ spec });
367
+ const apiReferencePages = [
368
+ // overview page
369
+ path.posix.join(basePrefixInOutput, 'index.html'),
370
+ // all route pages
371
+ ...routeList.map((r) => path.posix.join(basePrefixInOutput, r.slug, 'index.html')),
372
+ ];
373
+
360
374
  const manifest = {
361
375
  astroBase,
376
+ apiReferencePages,
362
377
  };
363
378
  await writeFile(path.join(stainlessDir, 'stl-manifest.json'), JSON.stringify(manifest, null, 2));
364
379
 
365
- const specComposite = await resolveSpecs();
366
-
367
380
  await buildAlgoliaIndex({
368
381
  specComposite,
369
382
  logger,
@@ -375,9 +388,6 @@ function stlStarlightAstroIntegration(pluginConfig: NormalizedStainlessStarlight
375
388
  // in this file so Cloudflare can serve them with a 404 status. These pages display helpful information
376
389
  // about the missing method and provide links to SDKs where it is available.
377
390
 
378
- // TODO: (multi-spec) support multiple specs
379
- const spec = specComposite.listUniqueSpecs()[0]!.data.sdkJson;
380
-
381
391
  const missingRoutes = generateMissingRouteList({
382
392
  spec,
383
393
  basePath: path.posix.join(astroBase, pluginConfig.basePath),
@@ -277,7 +277,7 @@ export function RenderSpec({
277
277
  const resource = getResourceFromSpec(path, spec);
278
278
 
279
279
  if (!resource || !parsed) {
280
- console.warn(`Could not find resource or parsed path for '${path}'`); // eslint-disable-line @eslint-react/purity
280
+ console.warn(`Could not find resource or parsed path for '${path}'`);
281
281
  return null;
282
282
  }
283
283
 
@@ -329,7 +329,7 @@ export function RenderMethod({ path }: { path: string }) {
329
329
  const resource = getResourceFromSpec(path, spec);
330
330
 
331
331
  if (!resource || !parsed) {
332
- console.warn(`Could not find resource or parsed path for '${path}'`); // eslint-disable-line @eslint-react/purity
332
+ console.warn(`Could not find resource or parsed path for '${path}'`);
333
333
  return null;
334
334
  }
335
335
 
@@ -0,0 +1,130 @@
1
+ import type { Dirent } from 'fs';
2
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
3
+ import { join } from 'path';
4
+
5
+ vi.mock('fs/promises', () => ({
6
+ readdir: vi.fn(),
7
+ readFile: vi.fn(),
8
+ }));
9
+
10
+ import { readdir, readFile } from 'fs/promises';
11
+ import { getProsePages } from './getProsePages';
12
+
13
+ const mockedReaddir = vi.mocked(readdir);
14
+ const mockedReadFile = vi.mocked(readFile);
15
+
16
+ function fakeDirent(parentPath: string, name: string, isFile = true): Dirent {
17
+ return {
18
+ name,
19
+ parentPath,
20
+ path: parentPath,
21
+ isFile: () => isFile,
22
+ isDirectory: () => !isFile,
23
+ isBlockDevice: () => false,
24
+ isCharacterDevice: () => false,
25
+ isFIFO: () => false,
26
+ isSocket: () => false,
27
+ isSymbolicLink: () => false,
28
+ } as Dirent;
29
+ }
30
+
31
+ function stubManifest(apiReferencePages: string[]) {
32
+ mockedReadFile.mockResolvedValue(JSON.stringify({ astroBase: '/', apiReferencePages }));
33
+ }
34
+
35
+ function stubReaddir(dirents: Dirent[]) {
36
+ // readdir has many overloads; cast to match the withFileTypes variant
37
+ mockedReaddir.mockResolvedValue(dirents as unknown as Awaited<ReturnType<typeof readdir>>);
38
+ }
39
+
40
+ describe('getProsePages', () => {
41
+ beforeEach(() => {
42
+ vi.resetAllMocks();
43
+ // Default: no manifest
44
+ mockedReadFile.mockRejectedValue(new Error('ENOENT'));
45
+ });
46
+
47
+ it('does not return API reference pages as prose when basePath is ""', async () => {
48
+ stubManifest(['resources/users/index.html', 'resources/users/methods/create/index.html']);
49
+ stubReaddir([
50
+ fakeDirent('/out/guides', 'intro.html'),
51
+ fakeDirent('/out/resources/users', 'index.html'),
52
+ fakeDirent('/out/resources/users/methods/create', 'index.html'),
53
+ ]);
54
+
55
+ const result = await getProsePages({ outputBasePath: '/out' });
56
+
57
+ // resources/users/** are API reference pages, not prose
58
+ expect(result).not.toContainEqual(join('/out/resources/users', 'index.html'));
59
+ expect(result).not.toContainEqual(join('/out/resources/users/methods/create', 'index.html'));
60
+ expect(result).toContainEqual(join('/out/guides', 'intro.html'));
61
+ });
62
+
63
+ it('returns all HTML files when no manifest exists', async () => {
64
+ stubReaddir([fakeDirent('/out', 'index.html'), fakeDirent('/out/guides', 'intro.html')]);
65
+
66
+ const result = await getProsePages({ outputBasePath: '/out' });
67
+
68
+ expect(result).toEqual([join('/out', 'index.html'), join('/out/guides', 'intro.html')]);
69
+ });
70
+
71
+ it('returns all HTML files when manifest has empty apiReferencePages', async () => {
72
+ stubManifest([]);
73
+ stubReaddir([fakeDirent('/out', 'index.html'), fakeDirent('/out/guides', 'intro.html')]);
74
+
75
+ const result = await getProsePages({ outputBasePath: '/out' });
76
+
77
+ expect(result).toEqual([join('/out', 'index.html'), join('/out/guides', 'intro.html')]);
78
+ });
79
+
80
+ it('excludes files listed in the manifest', async () => {
81
+ stubManifest(['api/resources/users/index.html', 'api/resources/users/methods/create/index.html']);
82
+ stubReaddir([
83
+ fakeDirent('/out', 'index.html'),
84
+ fakeDirent('/out/guides', 'intro.html'),
85
+ fakeDirent('/out/api/resources/users', 'index.html'),
86
+ fakeDirent('/out/api/resources/users/methods/create', 'index.html'),
87
+ ]);
88
+
89
+ const result = await getProsePages({ outputBasePath: '/out' });
90
+
91
+ expect(result).toEqual([join('/out', 'index.html'), join('/out/guides', 'intro.html')]);
92
+ });
93
+
94
+ it('excludes non-HTML files', async () => {
95
+ stubReaddir([
96
+ fakeDirent('/out', 'index.html'),
97
+ fakeDirent('/out', 'style.css'),
98
+ fakeDirent('/out', 'app.js'),
99
+ ]);
100
+
101
+ const result = await getProsePages({ outputBasePath: '/out' });
102
+
103
+ expect(result).toEqual([join('/out', 'index.html')]);
104
+ });
105
+
106
+ it('excludes directories', async () => {
107
+ stubReaddir([fakeDirent('/out', 'index.html'), fakeDirent('/out', 'guides', false)]);
108
+
109
+ const result = await getProsePages({ outputBasePath: '/out' });
110
+
111
+ expect(result).toEqual([join('/out', 'index.html')]);
112
+ });
113
+
114
+ it('returns empty when all HTML files are in the manifest', async () => {
115
+ stubManifest(['index.html', 'resources/users/index.html']);
116
+ stubReaddir([fakeDirent('/out', 'index.html'), fakeDirent('/out/resources/users', 'index.html')]);
117
+
118
+ const result = await getProsePages({ outputBasePath: '/out' });
119
+
120
+ expect(result).toEqual([]);
121
+ });
122
+
123
+ it('returns empty when output has no HTML files', async () => {
124
+ stubReaddir([fakeDirent('/out', 'style.css')]);
125
+
126
+ const result = await getProsePages({ outputBasePath: '/out' });
127
+
128
+ expect(result).toEqual([]);
129
+ });
130
+ });
@@ -1,6 +1,8 @@
1
- import { readdir } from 'fs/promises';
1
+ import { readdir, readFile } from 'fs/promises';
2
2
  import { join, relative } from 'path';
3
3
 
4
+ const MANIFEST_PATH = '_stainless/stl-manifest.json';
5
+
4
6
  /**
5
7
  * Get all prose pages after a build, by reading all HTML files from the
6
8
  * given output directory, and not from assets.
@@ -10,14 +12,11 @@ import { join, relative } from 'path';
10
12
  * Other astro integrations may hijack the "[...slug]" entrypoint, and any files
11
13
  * previously in the [...slug] asset map entry would be lost (this is where starlight stores
12
14
  * its prose HTML files).
15
+ *
16
+ * API reference pages are excluded using the manifest written by the API
17
+ * reference plugin at build time, rather than guessing based on path prefix.
13
18
  */
14
- export async function getProsePages({
15
- apiReferenceBasePath,
16
- outputBasePath,
17
- }: {
18
- apiReferenceBasePath: string | null;
19
- outputBasePath: string;
20
- }): Promise<string[]> {
19
+ export async function getProsePages({ outputBasePath }: { outputBasePath: string }): Promise<string[]> {
21
20
  const allFiles = await readdir(outputBasePath, {
22
21
  recursive: true,
23
22
  withFileTypes: true,
@@ -27,15 +26,22 @@ export async function getProsePages({
27
26
  .filter((file) => file.isFile() && file.name.endsWith('.html'))
28
27
  .map((file) => join(file.parentPath, file.name));
29
28
 
30
- if (!apiReferenceBasePath) return htmlFiles;
31
- // Normalize by removing leading/trailing slashes from apiReferenceBasePath
32
- const normalizedApiPath = apiReferenceBasePath.replace(/^\/+/g, '').replace(/\/+$/g, '');
33
- const pagesToRender = htmlFiles.filter((absPath) => {
29
+ const apiReferencePages = await readApiReferencePagesFromManifest(outputBasePath);
30
+ if (apiReferencePages.size === 0) return htmlFiles;
31
+
32
+ return htmlFiles.filter((absPath) => {
34
33
  const relPath = relative(outputBasePath, absPath);
35
- // Filter out API reference pages
36
- if (relPath === normalizedApiPath || relPath.startsWith(`${normalizedApiPath}/`)) return false;
37
- return true;
34
+ return !apiReferencePages.has(relPath);
38
35
  });
36
+ }
39
37
 
40
- return pagesToRender;
38
+ async function readApiReferencePagesFromManifest(outputBasePath: string): Promise<Set<string>> {
39
+ try {
40
+ const raw = await readFile(join(outputBasePath, MANIFEST_PATH), 'utf-8');
41
+ const manifest = JSON.parse(raw) as { apiReferencePages?: string[] };
42
+ const pages = manifest.apiReferencePages ?? [];
43
+ return new Set(pages);
44
+ } catch {
45
+ return new Set();
46
+ }
41
47
  }
package/stl-docs/index.ts CHANGED
@@ -303,17 +303,17 @@ export function stainlessDocs(config: StainlessDocsUserConfig): AstroIntegration
303
303
  stainlessDocsIntegration(normalizedConfig, apiReferenceBasePath),
304
304
  conditionalIntegration({
305
305
  condition: !config.experimental?.disableProseMarkdownRendering,
306
- integration: stainlessDocsMarkdownRenderer({ apiReferenceBasePath }),
306
+ integration: stainlessDocsMarkdownRenderer(),
307
307
  reason: 'disabled by experimental config "disableProseMarkdownRendering"',
308
308
  }),
309
309
  conditionalIntegration({
310
310
  condition: !config.experimental?.disableStainlessProseIndexing,
311
- integration: stainlessDocsAlgoliaProseIndexing({ apiReferenceBasePath }),
311
+ integration: stainlessDocsAlgoliaProseIndexing(),
312
312
  reason: 'disabled by experimental config "disableStainlessProseIndexing"',
313
313
  }),
314
314
  conditionalIntegration({
315
315
  condition: !config.experimental?.disableStainlessProseIndexing,
316
- integration: stainlessDocsVectorProseIndexing(normalizedConfig, apiReferenceBasePath),
316
+ integration: stainlessDocsVectorProseIndexing(normalizedConfig),
317
317
  reason: 'disabled by experimental config "disableStainlessProseIndexing"',
318
318
  }),
319
319
  ];
@@ -304,10 +304,7 @@ async function syncProseDocuments(opts: {
304
304
 
305
305
  // ─── Astro integration ──────────────────────────────────────────────
306
306
 
307
- export function stainlessDocsVectorProseIndexing(
308
- config: NormalizedStainlessDocsConfig,
309
- apiReferenceBasePath: string | null,
310
- ): AstroIntegration {
307
+ export function stainlessDocsVectorProseIndexing(config: NormalizedStainlessDocsConfig): AstroIntegration {
311
308
  return {
312
309
  name: 'stl-docs-prose-indexing',
313
310
  hooks: {
@@ -331,7 +328,7 @@ export function stainlessDocsVectorProseIndexing(
331
328
  return;
332
329
  }
333
330
 
334
- const pages = await getProsePages({ apiReferenceBasePath, outputBasePath });
331
+ const pages = await getProsePages({ outputBasePath });
335
332
  if (pages.length === 0) {
336
333
  logger.info('No prose pages found to index for vector search');
337
334
  return;
@@ -6,11 +6,7 @@ import { getSharedLogger } from '../../shared/getSharedLogger';
6
6
  import { bold } from '../../shared/terminalUtils';
7
7
  import { getProsePages } from '../../shared/getProsePages';
8
8
 
9
- export function stainlessDocsMarkdownRenderer({
10
- apiReferenceBasePath,
11
- }: {
12
- apiReferenceBasePath: string | null;
13
- }): AstroIntegration {
9
+ export function stainlessDocsMarkdownRenderer(): AstroIntegration {
14
10
  return {
15
11
  name: 'stl-docs-md',
16
12
  hooks: {
@@ -23,7 +19,7 @@ export function stainlessDocsMarkdownRenderer({
23
19
  'astro:build:done': async ({ logger: localLogger, dir }) => {
24
20
  const logger = getSharedLogger({ fallback: localLogger });
25
21
  const outputBasePath = dir.pathname;
26
- const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
22
+ const pagesToRender = await getProsePages({ outputBasePath });
27
23
 
28
24
  logger.info(bold(`Building ${pagesToRender.length} Markdown pages for prose content`));
29
25
 
@@ -174,11 +174,7 @@ export function* indexHTML(
174
174
  }
175
175
  }
176
176
 
177
- export function stainlessDocsAlgoliaProseIndexing({
178
- apiReferenceBasePath,
179
- }: {
180
- apiReferenceBasePath: string | null;
181
- }): AstroIntegration {
177
+ export function stainlessDocsAlgoliaProseIndexing(): AstroIntegration {
182
178
  return {
183
179
  name: 'stl-docs-prose-indexing',
184
180
  hooks: {
@@ -197,7 +193,7 @@ export function stainlessDocsAlgoliaProseIndexing({
197
193
  return;
198
194
  }
199
195
 
200
- const pagesToRender = await getProsePages({ apiReferenceBasePath, outputBasePath });
196
+ const pagesToRender = await getProsePages({ outputBasePath });
201
197
  logger.info(bold(`Indexing ${pagesToRender.length} prose pages for algolia search`));
202
198
 
203
199
  const objects = [];