mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 22:09:04 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 404c8df073 | |||
| 05e1d8cd96 | |||
| 9f0de74caa | |||
| a820739bd2 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Remove unused search api method from gitbook/api/lib
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@gitbook/react-openapi': patch
|
||||
---
|
||||
|
||||
Hide x-gitbook-\* symbols in OpenAPI blocks
|
||||
@@ -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
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"packages/colors": {
|
||||
"name": "@gitbook/colors",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.3",
|
||||
},
|
||||
@@ -38,9 +38,9 @@
|
||||
},
|
||||
"packages/gitbook": {
|
||||
"name": "gitbook",
|
||||
"version": "0.6.1",
|
||||
"version": "0.6.2",
|
||||
"dependencies": {
|
||||
"@gitbook/api": "^0.93.0",
|
||||
"@gitbook/api": "^0.95.0",
|
||||
"@gitbook/cache-do": "workspace:*",
|
||||
"@gitbook/colors": "workspace:*",
|
||||
"@gitbook/emoji-codepoints": "workspace:*",
|
||||
@@ -155,7 +155,7 @@
|
||||
},
|
||||
"packages/openapi-parser": {
|
||||
"name": "@gitbook/openapi-parser",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"dependencies": {
|
||||
"@scalar/openapi-parser": "^0.10.4",
|
||||
"@scalar/openapi-types": "^0.1.6",
|
||||
@@ -211,7 +211,7 @@
|
||||
},
|
||||
"packages/react-openapi": {
|
||||
"name": "@gitbook/react-openapi",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"dependencies": {
|
||||
"@gitbook/openapi-parser": "workspace:*",
|
||||
"@scalar/api-client-react": "1.0.87",
|
||||
@@ -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.95.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-9KAbt27Ile6cqAch7QEbiJHALQHojYlhsPzilgdQ5wpHgLwsrd7Smd58A3/8bWBKq4KV0vP4rh3oYhIw+LlWFw=="],
|
||||
|
||||
"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.95.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,42 @@ export const getUserById = cache({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -1066,55 +1102,6 @@ export async function getSpaceContentData(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search content in a space.
|
||||
*/
|
||||
export const searchSpaceContent = cache({
|
||||
name: 'api.searchSpaceContent',
|
||||
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (
|
||||
spaceId: string,
|
||||
/** The revision ID is used as a cache bust key, to avoid revalidating lot of cache entries by tags */
|
||||
revisionId: string,
|
||||
query: string,
|
||||
options: CacheFunctionOptions,
|
||||
) => {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.spaces.searchSpaceContent(
|
||||
spaceId,
|
||||
{ query },
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
},
|
||||
);
|
||||
return cacheResponse(response);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Search content accross all spaces in a parent (site or collection).
|
||||
*/
|
||||
export const searchParentContent = cache({
|
||||
name: 'api.searchParentContent',
|
||||
tag: (spaceId) => getAPICacheTag({ tag: 'space', space: spaceId }),
|
||||
getKeySuffix: getAPIContextId,
|
||||
get: async (parentId: string, query: string, options: CacheFunctionOptions) => {
|
||||
const apiCtx = await api();
|
||||
const response = await apiCtx.client.search.searchContent(
|
||||
{ query },
|
||||
{
|
||||
...noCacheFetchOptions,
|
||||
signal: options.signal,
|
||||
},
|
||||
);
|
||||
return cacheResponse(response, {
|
||||
ttl: 60 * 60,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Search content in a Site or specific SiteSpaces.
|
||||
*/
|
||||
@@ -1275,6 +1262,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) {
|
||||
@@ -1298,6 +1291,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 openAPISpecVersionContent = await getLatestOpenAPISpecVersionContent(
|
||||
organizationId,
|
||||
contentRef.spec,
|
||||
);
|
||||
|
||||
if (!openAPISpecVersionContent) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
href: openAPISpecVersionContent.url,
|
||||
text: contentRef.spec,
|
||||
active: false,
|
||||
openAPIFilesystem: openAPISpecVersionContent.filesystem 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,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { checkIsReference, createStateKey, resolveDescription } from './utils';
|
||||
import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
|
||||
import { InteractiveSection } from './InteractiveSection';
|
||||
import { json2xml } from './json2xml';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
|
||||
/**
|
||||
* Display an example of the response content.
|
||||
@@ -226,7 +227,7 @@ function stringifyExample(args: { example: OpenAPIV3.ExampleObject; xml: boolean
|
||||
return json2xml(example.value);
|
||||
}
|
||||
|
||||
return JSON.stringify(example.value, null, 2);
|
||||
return stringifyOpenAPI(example.value, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
/**
|
||||
* Stringify an OpenAPI object. Same API as JSON.stringify.
|
||||
*/
|
||||
export function stringifyOpenAPI(body: unknown, transformer?: null, indent?: number): string {
|
||||
return JSON.stringify(body, transformer, indent);
|
||||
export function stringifyOpenAPI(body: unknown, _?: null, indent?: number): string {
|
||||
return JSON.stringify(
|
||||
body,
|
||||
(key, value) => {
|
||||
// Ignore internal keys
|
||||
if (key.startsWith('x-gitbook-')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
indent,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user