Skip to content

Commit 9f0de74

Browse files
authored
Add support for new OpenAPI ref (#2860)
1 parent a820739 commit 9f0de74

File tree

10 files changed

+169
-19
lines changed

10 files changed

+169
-19
lines changed

.changeset/perfect-donuts-hear.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Add support for new OpenAPI ref

.changeset/poor-rats-trade.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@gitbook/react-openapi': patch
3+
---
4+
5+
Fix ID not set when there is no operation summary

bun.lock

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"name": "gitbook",
4141
"version": "0.6.1",
4242
"dependencies": {
43-
"@gitbook/api": "^0.93.0",
43+
"@gitbook/api": "^0.94.0",
4444
"@gitbook/cache-do": "workspace:*",
4545
"@gitbook/colors": "workspace:*",
4646
"@gitbook/emoji-codepoints": "workspace:*",
@@ -4651,6 +4651,8 @@
46514651

46524652
"gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
46534653

4654+
"gitbook/@gitbook/api": ["@gitbook/api@0.94.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-jOvqUSdyXeuPpBiujkQLb14uVQA5A0XL+P89MmC/53hV7v/8gR8WlJN9RJVDrP0LX51dsLT+/zYN8xWp19nPwA=="],
4655+
46544656
"gitbook-v2/next": ["next@15.2.0-canary.45", "", { "dependencies": { "@next/env": "15.2.0-canary.45", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.2.0-canary.45", "@next/swc-darwin-x64": "15.2.0-canary.45", "@next/swc-linux-arm64-gnu": "15.2.0-canary.45", "@next/swc-linux-arm64-musl": "15.2.0-canary.45", "@next/swc-linux-x64-gnu": "15.2.0-canary.45", "@next/swc-linux-x64-musl": "15.2.0-canary.45", "@next/swc-win32-arm64-msvc": "15.2.0-canary.45", "@next/swc-win32-x64-msvc": "15.2.0-canary.45", "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-UsneTQn9tntbiAaXpvoXhhsTBb58Q2XIs2Dfka+qWA8motBz0ZvW297YHLxhdur4xN0IJvknnZKl5Bs7wAGlOg=="],
46554657

46564658
"glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],

packages/gitbook/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
1818
},
1919
"dependencies": {
20-
"@gitbook/api": "^0.93.0",
20+
"@gitbook/api": "^0.94.0",
2121
"@gitbook/cache-do": "workspace:*",
2222
"@gitbook/colors": "workspace:*",
2323
"@gitbook/emoji-codepoints": "workspace:*",

packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Icon } from '@gitbook/icons';
33
import { OpenAPIOperation } from '@gitbook/react-openapi';
44
import React from 'react';
55

6-
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
6+
import { resolveOpenAPIBlock } from '@/lib/openapi/fetch';
77
import { tcls } from '@/lib/tailwind';
88

99
import { BlockProps } from '../Block';
@@ -27,13 +27,17 @@ export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
2727

2828
async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
2929
const { block, context } = props;
30-
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
30+
31+
const { data, specUrl, error } = await resolveOpenAPIBlock({
32+
block,
33+
context: { resolveContentRef: context.resolveContentRef },
34+
});
3135

3236
if (error) {
3337
return (
34-
<div className={tcls('hidden')}>
38+
<div className="hidden">
3539
<p>
36-
Error with {error.rootURL}: {error.message}
40+
Error with {specUrl}: {error.message}
3741
</p>
3842
</div>
3943
);

packages/gitbook/src/lib/api.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,78 @@ export const getUserById = cache({
208208
},
209209
});
210210

211+
/**
212+
* Get the latest version of an OpenAPI spec by its slug.
213+
*/
214+
export const getLatestOpenAPISpecVersion = cache({
215+
name: 'api.getLatestOpenApiSpecVersion',
216+
tag: (organization, openAPISpec) =>
217+
getAPICacheTag({
218+
tag: 'openapi',
219+
organization,
220+
openAPISpec,
221+
}),
222+
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
223+
try {
224+
const apiCtx = await api();
225+
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersion(
226+
organizationId,
227+
slug,
228+
{
229+
...noCacheFetchOptions,
230+
signal: options.signal,
231+
},
232+
);
233+
return cacheResponse(response, { revalidateBefore: 60 * 60 });
234+
} catch (error) {
235+
if (checkHasErrorCode(error, 404)) {
236+
return {
237+
revalidateBefore: 5,
238+
data: null,
239+
};
240+
}
241+
242+
throw error;
243+
}
244+
},
245+
});
246+
247+
/**
248+
* Get the latest version of an OpenAPI spec by its slug.
249+
*/
250+
export const getLatestOpenAPISpecVersionContent = cache({
251+
name: 'api.getLatestOpenApiSpecVersionContent',
252+
tag: (organization, openAPISpec) =>
253+
getAPICacheTag({
254+
tag: 'openapi',
255+
organization,
256+
openAPISpec,
257+
}),
258+
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
259+
try {
260+
const apiCtx = await api();
261+
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersionContent(
262+
organizationId,
263+
slug,
264+
{
265+
...noCacheFetchOptions,
266+
signal: options.signal,
267+
},
268+
);
269+
return cacheResponse(response, { revalidateBefore: 60 * 60 });
270+
} catch (error) {
271+
if (checkHasErrorCode(error, 404)) {
272+
return {
273+
revalidateBefore: 5,
274+
data: null,
275+
};
276+
}
277+
278+
throw error;
279+
}
280+
},
281+
});
282+
211283
/**
212284
* Resolve a URL to the content to render.
213285
*/
@@ -1226,6 +1298,12 @@ export function getAPICacheTag(
12261298
| {
12271299
tag: 'site';
12281300
site: string;
1301+
}
1302+
// All data related to an OpenAPI spec
1303+
| {
1304+
tag: 'openapi';
1305+
organization: string;
1306+
openAPISpec: string;
12291307
},
12301308
): string {
12311309
switch (spec.tag) {
@@ -1249,6 +1327,8 @@ export function getAPICacheTag(
12491327
return `site:${spec.site}`;
12501328
case 'integration':
12511329
return `integration:${spec.integration}`;
1330+
case 'openapi':
1331+
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
12521332
default:
12531333
assertNever(spec);
12541334
}

packages/gitbook/src/lib/document-sections.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { JSONDocument, ContentRef } from '@gitbook/api';
22

33
import { getNodeText } from './document';
4-
import { fetchOpenAPIBlock } from './openapi/fetch';
4+
import { resolveOpenAPIBlock } from './openapi/fetch';
55
import { ResolvedContentRef } from './references';
66

77
export interface DocumentSection {
@@ -38,7 +38,10 @@ export async function getDocumentSections(
3838
}
3939

4040
if (block.type === 'swagger' && block.meta?.id) {
41-
const { data: operation } = await fetchOpenAPIBlock(block, resolveContentRef);
41+
const { data: operation } = await resolveOpenAPIBlock({
42+
block,
43+
context: { resolveContentRef },
44+
});
4245
if (operation) {
4346
sections.push({
4447
id: block.meta.id,

packages/gitbook/src/lib/openapi/fetch.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,48 @@ import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
77
import { enrichFilesystem } from './enrich';
88
import { ResolvedContentRef } from '../references';
99

10+
const weakmap = new WeakMap<DocumentBlockOpenAPI, ResolveOpenAPIBlockResult>();
11+
1012
/**
11-
* Fetch an OpenAPI specification for an operation.
13+
* Cache the result of resolving an OpenAPI block.
14+
* It is important because the resolve is called in sections and in the block itself.
1215
*/
13-
export async function fetchOpenAPIBlock(
14-
block: DocumentBlockOpenAPI,
15-
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>,
16-
): Promise<
17-
| { data: OpenAPIOperationData | null; specUrl: string | null; error?: undefined }
16+
export function resolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
17+
if (weakmap.has(args.block)) {
18+
return weakmap.get(args.block)!;
19+
}
20+
21+
const result = baseResolveOpenAPIBlock(args);
22+
weakmap.set(args.block, result);
23+
return result;
24+
}
25+
26+
type ResolveOpenAPIBlockArgs = {
27+
block: DocumentBlockOpenAPI;
28+
context: { resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null> };
29+
};
30+
type ResolveOpenAPIBlockResult = Promise<
31+
| { error?: undefined; data: OpenAPIOperationData | null; specUrl: string | null }
1832
| { error: OpenAPIParseError; data?: undefined; specUrl?: undefined }
19-
> {
20-
const resolved = block.data.ref ? await resolveContentRef(block.data.ref) : null;
21-
if (!resolved || !block.data.path || !block.data.method) {
33+
>;
34+
/**
35+
* Resolve OpenAPI block.
36+
*/
37+
async function baseResolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
38+
const { context, block } = args;
39+
if (!block.data.path || !block.data.method) {
40+
return { data: null, specUrl: null };
41+
}
42+
43+
const resolved = block.data.ref ? await context.resolveContentRef(block.data.ref) : null;
44+
45+
if (!resolved) {
2246
return { data: null, specUrl: null };
2347
}
2448

2549
try {
26-
const filesystem = await fetchFilesystem(resolved.href);
50+
const filesystem = resolved.openAPIFilesystem ?? (await fetchFilesystem(resolved.href));
51+
2752
const data = await resolveOpenAPIOperation(filesystem, {
2853
path: block.data.path,
2954
method: block.data.method,

packages/gitbook/src/lib/references.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SiteSpace,
88
Space,
99
} from '@gitbook/api';
10+
import type { Filesystem } from '@gitbook/openapi-parser';
1011
import assertNever from 'assert-never';
1112
import React from 'react';
1213

@@ -17,6 +18,8 @@ import {
1718
SpaceContentPointer,
1819
getCollection,
1920
getDocument,
21+
getLatestOpenAPISpecVersion,
22+
getLatestOpenAPISpecVersionContent,
2023
getPageDocument,
2124
getPublishedContentSite,
2225
getReusableContent,
@@ -50,6 +53,8 @@ export interface ResolvedContentRef {
5053
file?: RevisionFile;
5154
/** Resolved reusable content, if the ref points to reusable content on a revision. */
5255
reusableContent?: RevisionReusableContent;
56+
/** Resolve OpenAPI spec filesystem. */
57+
openAPIFilesystem?: Filesystem;
5358
}
5459

5560
export interface ContentRefContext extends PageHrefContext {
@@ -272,6 +277,27 @@ export async function resolveContentRef(
272277
};
273278
}
274279

280+
case 'openapi': {
281+
if (!siteContext) {
282+
return null;
283+
}
284+
const { organizationId } = siteContext;
285+
const [openAPISpecVersion, openAPISpecVersionContent] = await Promise.all([
286+
getLatestOpenAPISpecVersion(organizationId, contentRef.spec),
287+
getLatestOpenAPISpecVersionContent(organizationId, contentRef.spec),
288+
]);
289+
290+
if (!openAPISpecVersion || !openAPISpecVersionContent) {
291+
return null;
292+
}
293+
return {
294+
href: openAPISpecVersion.url,
295+
text: contentRef.spec,
296+
active: false,
297+
openAPIFilesystem: openAPISpecVersionContent as Filesystem,
298+
};
299+
}
300+
275301
default:
276302
assertNever(contentRef);
277303
}

packages/react-openapi/src/OpenAPIOperation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function OpenAPIOperation(props: {
2929

3030
return (
3131
<div className={clsx('openapi-operation', className)}>
32-
<div className="openapi-summary">
32+
<div className="openapi-summary" id={operation.summary ? undefined : context.id}>
3333
{operation.summary
3434
? context.renderHeading({
3535
deprecated: operation.deprecated ?? false,

0 commit comments

Comments
 (0)