mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Add support for new OpenAPI ref (#2860)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Add support for new OpenAPI ref
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Fix ID not set when there is no operation summary
|
||||
@@ -40,7 +40,7 @@
|
||||
"name": "gitbook",
|
||||
"version": "0.6.1",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.93.0",
|
||||
"@gitbook/api": "^0.94.0",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
@@ -4651,6 +4651,8 @@
|
||||
|
||||
"gaxios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static/icons && rm -rf ./public/~gitbook/static/math"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.93.0",
|
||||
"@gitbook/api": "^0.94.0",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Icon } from '@gitbook/icons';
|
||||
import { OpenAPIOperation } from '@gitbook/react-openapi';
|
||||
import React from 'react';
|
||||
|
||||
import { fetchOpenAPIBlock } from '@/lib/openapi/fetch';
|
||||
import { resolveOpenAPIBlock } from '@/lib/openapi/fetch';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { BlockProps } from '../Block';
|
||||
@@ -27,13 +27,17 @@ export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
|
||||
|
||||
async function OpenAPIBody(props: BlockProps<DocumentBlockOpenAPI>) {
|
||||
const { block, context } = props;
|
||||
const { data, specUrl, error } = await fetchOpenAPIBlock(block, context.resolveContentRef);
|
||||
|
||||
const { data, specUrl, error } = await resolveOpenAPIBlock({
|
||||
block,
|
||||
context: { resolveContentRef: context.resolveContentRef },
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className={tcls('hidden')}>
|
||||
<div className="hidden">
|
||||
<p>
|
||||
Error with {error.rootURL}: {error.message}
|
||||
Error with {specUrl}: {error.message}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -208,6 +208,78 @@ export const getUserById = cache({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the latest version of an OpenAPI spec by its slug.
|
||||
*/
|
||||
export const getLatestOpenAPISpecVersion = cache({
|
||||
name: 'api.getLatestOpenApiSpecVersion',
|
||||
tag: (organization, openAPISpec) =>
|
||||
getAPICacheTag({
|
||||
tag: 'openapi',
|
||||
organization,
|
||||
openAPISpec,
|
||||
}),
|
||||
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
|
||||
try {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersion(
|
||||
organizationId,
|
||||
slug,
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
},
|
||||
);
|
||||
return cacheResponse(response, { revalidateBefore: 60 * 60 });
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return {
|
||||
revalidateBefore: 5,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Get the latest version of an OpenAPI spec by its slug.
|
||||
*/
|
||||
export const getLatestOpenAPISpecVersionContent = cache({
|
||||
name: 'api.getLatestOpenApiSpecVersionContent',
|
||||
tag: (organization, openAPISpec) =>
|
||||
getAPICacheTag({
|
||||
tag: 'openapi',
|
||||
organization,
|
||||
openAPISpec,
|
||||
}),
|
||||
get: async (organizationId: string, slug: string, options: CacheFunctionOptions) => {
|
||||
try {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.orgs.getLatestOpenApiSpecVersionContent(
|
||||
organizationId,
|
||||
slug,
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
},
|
||||
);
|
||||
return cacheResponse(response, { revalidateBefore: 60 * 60 });
|
||||
} catch (error) {
|
||||
if (checkHasErrorCode(error, 404)) {
|
||||
return {
|
||||
revalidateBefore: 5,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolve a URL to the content to render.
|
||||
*/
|
||||
@@ -1226,6 +1298,12 @@ export function getAPICacheTag(
|
||||
| {
|
||||
tag: 'site';
|
||||
site: string;
|
||||
}
|
||||
// All data related to an OpenAPI spec
|
||||
| {
|
||||
tag: 'openapi';
|
||||
organization: string;
|
||||
openAPISpec: string;
|
||||
},
|
||||
): string {
|
||||
switch (spec.tag) {
|
||||
@@ -1249,6 +1327,8 @@ export function getAPICacheTag(
|
||||
return `site:${spec.site}`;
|
||||
case 'integration':
|
||||
return `integration:${spec.integration}`;
|
||||
case 'openapi':
|
||||
return `organization:${spec.organization}:openapi:${spec.openAPISpec}`;
|
||||
default:
|
||||
assertNever(spec);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { JSONDocument, ContentRef } from '@gitbook/api';
|
||||
|
||||
import { getNodeText } from './document';
|
||||
import { fetchOpenAPIBlock } from './openapi/fetch';
|
||||
import { resolveOpenAPIBlock } from './openapi/fetch';
|
||||
import { ResolvedContentRef } from './references';
|
||||
|
||||
export interface DocumentSection {
|
||||
@@ -38,7 +38,10 @@ export async function getDocumentSections(
|
||||
}
|
||||
|
||||
if (block.type === 'swagger' && block.meta?.id) {
|
||||
const { data: operation } = await fetchOpenAPIBlock(block, resolveContentRef);
|
||||
const { data: operation } = await resolveOpenAPIBlock({
|
||||
block,
|
||||
context: { resolveContentRef },
|
||||
});
|
||||
if (operation) {
|
||||
sections.push({
|
||||
id: block.meta.id,
|
||||
|
||||
@@ -7,23 +7,48 @@ import { cache, noCacheFetchOptions, CacheFunctionOptions } from '@/lib/cache';
|
||||
import { enrichFilesystem } from './enrich';
|
||||
import { ResolvedContentRef } from '../references';
|
||||
|
||||
const weakmap = new WeakMap<DocumentBlockOpenAPI, ResolveOpenAPIBlockResult>();
|
||||
|
||||
/**
|
||||
* Fetch an OpenAPI specification for an operation.
|
||||
* Cache the result of resolving an OpenAPI block.
|
||||
* It is important because the resolve is called in sections and in the block itself.
|
||||
*/
|
||||
export async function fetchOpenAPIBlock(
|
||||
block: DocumentBlockOpenAPI,
|
||||
resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null>,
|
||||
): Promise<
|
||||
| { data: OpenAPIOperationData | null; specUrl: string | null; error?: undefined }
|
||||
export function resolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
|
||||
if (weakmap.has(args.block)) {
|
||||
return weakmap.get(args.block)!;
|
||||
}
|
||||
|
||||
const result = baseResolveOpenAPIBlock(args);
|
||||
weakmap.set(args.block, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
type ResolveOpenAPIBlockArgs = {
|
||||
block: DocumentBlockOpenAPI;
|
||||
context: { resolveContentRef: (ref: ContentRef) => Promise<ResolvedContentRef | null> };
|
||||
};
|
||||
type ResolveOpenAPIBlockResult = Promise<
|
||||
| { error?: undefined; data: OpenAPIOperationData | null; specUrl: string | null }
|
||||
| { error: OpenAPIParseError; data?: undefined; specUrl?: undefined }
|
||||
> {
|
||||
const resolved = block.data.ref ? await resolveContentRef(block.data.ref) : null;
|
||||
if (!resolved || !block.data.path || !block.data.method) {
|
||||
>;
|
||||
/**
|
||||
* Resolve OpenAPI block.
|
||||
*/
|
||||
async function baseResolveOpenAPIBlock(args: ResolveOpenAPIBlockArgs): ResolveOpenAPIBlockResult {
|
||||
const { context, block } = args;
|
||||
if (!block.data.path || !block.data.method) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
const resolved = block.data.ref ? await context.resolveContentRef(block.data.ref) : null;
|
||||
|
||||
if (!resolved) {
|
||||
return { data: null, specUrl: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const filesystem = await fetchFilesystem(resolved.href);
|
||||
const filesystem = resolved.openAPIFilesystem ?? (await fetchFilesystem(resolved.href));
|
||||
|
||||
const data = await resolveOpenAPIOperation(filesystem, {
|
||||
path: block.data.path,
|
||||
method: block.data.method,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
SiteSpace,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import type { Filesystem } from '@gitbook/openapi-parser';
|
||||
import assertNever from 'assert-never';
|
||||
import React from 'react';
|
||||
|
||||
@@ -17,6 +18,8 @@ import {
|
||||
SpaceContentPointer,
|
||||
getCollection,
|
||||
getDocument,
|
||||
getLatestOpenAPISpecVersion,
|
||||
getLatestOpenAPISpecVersionContent,
|
||||
getPageDocument,
|
||||
getPublishedContentSite,
|
||||
getReusableContent,
|
||||
@@ -50,6 +53,8 @@ export interface ResolvedContentRef {
|
||||
file?: RevisionFile;
|
||||
/** Resolved reusable content, if the ref points to reusable content on a revision. */
|
||||
reusableContent?: RevisionReusableContent;
|
||||
/** Resolve OpenAPI spec filesystem. */
|
||||
openAPIFilesystem?: Filesystem;
|
||||
}
|
||||
|
||||
export interface ContentRefContext extends PageHrefContext {
|
||||
@@ -272,6 +277,27 @@ export async function resolveContentRef(
|
||||
};
|
||||
}
|
||||
|
||||
case 'openapi': {
|
||||
if (!siteContext) {
|
||||
return null;
|
||||
}
|
||||
const { organizationId } = siteContext;
|
||||
const [openAPISpecVersion, openAPISpecVersionContent] = await Promise.all([
|
||||
getLatestOpenAPISpecVersion(organizationId, contentRef.spec),
|
||||
getLatestOpenAPISpecVersionContent(organizationId, contentRef.spec),
|
||||
]);
|
||||
|
||||
if (!openAPISpecVersion || !openAPISpecVersionContent) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
href: openAPISpecVersion.url,
|
||||
text: contentRef.spec,
|
||||
active: false,
|
||||
openAPIFilesystem: openAPISpecVersionContent as Filesystem,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
assertNever(contentRef);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function OpenAPIOperation(props: {
|
||||
|
||||
return (
|
||||
<div className={clsx('openapi-operation', className)}>
|
||||
<div className="openapi-summary">
|
||||
<div className="openapi-summary" id={operation.summary ? undefined : context.id}>
|
||||
{operation.summary
|
||||
? context.renderHeading({
|
||||
deprecated: operation.deprecated ?? false,
|
||||
|
||||
Reference in New Issue
Block a user