mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 23:55:20 +00:00
Improve OpenAPI performances (#2784)
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/react-openapi': patch
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve OpenAPI rendering performances by caching markdown parsing
|
||||||
@@ -17,12 +17,10 @@ import './scalar.css';
|
|||||||
* Render an OpenAPI block.
|
* Render an OpenAPI block.
|
||||||
*/
|
*/
|
||||||
export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
|
export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
|
||||||
const { block, style } = props;
|
const { style } = props;
|
||||||
return (
|
return (
|
||||||
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
||||||
<React.Suspense fallback={<OpenAPIFallback />}>
|
<OpenAPIBody {...props} />
|
||||||
<OpenAPIBody {...props} />
|
|
||||||
</React.Suspense>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import { unified } from 'unified';
|
|||||||
* Parse markdown and output HTML.
|
* Parse markdown and output HTML.
|
||||||
*/
|
*/
|
||||||
export async function parseMarkdown(markdown: string): Promise<string> {
|
export async function parseMarkdown(markdown: string): Promise<string> {
|
||||||
const file = await unified()
|
const promise = unified()
|
||||||
.use(remarkParse)
|
.use(remarkParse)
|
||||||
.use(remarkGfm)
|
.use(remarkGfm)
|
||||||
.use(remarkRehype)
|
.use(remarkRehype)
|
||||||
.use(rehypeSanitize)
|
.use(rehypeSanitize)
|
||||||
.use(rehypeStringify)
|
.use(rehypeStringify)
|
||||||
.process(markdown);
|
.process(markdown)
|
||||||
|
.then((file) => file.toString());
|
||||||
|
|
||||||
return file.toString();
|
return promise;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export async function fetchOpenAPIBlock(
|
|||||||
|
|
||||||
const fetcher: OpenAPIFetcher = {
|
const fetcher: OpenAPIFetcher = {
|
||||||
fetch: cache({
|
fetch: cache({
|
||||||
name: 'openapi.fetch.v2',
|
name: 'openapi.fetch.v3',
|
||||||
get: async (url: string, options: CacheFunctionOptions) => {
|
get: async (url: string, options: CacheFunctionOptions) => {
|
||||||
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
|
// Wrap the raw string to prevent invalid URLs from being passed to fetch.
|
||||||
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
|
// This can happen if the URL has whitespace, which is currently handled differently by Cloudflare's implementation of fetch:
|
||||||
@@ -66,12 +66,11 @@ const fetcher: OpenAPIFetcher = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const data = await parseOpenAPI({ url, value: text });
|
const data = await parseOpenAPI({ url, value: text, parseMarkdown });
|
||||||
return {
|
return {
|
||||||
...parseCacheResponse(response),
|
...parseCacheResponse(response),
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
parseMarkdown,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ApiClientModalProvider } from '@scalar/api-client-react';
|
import { ApiClientModalProvider } from '@scalar/api-client-react';
|
||||||
|
|
||||||
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||||
import { Markdown } from './Markdown';
|
import { Markdown } from './Markdown';
|
||||||
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||||
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { parseOpenAPI } from './parser';
|
|||||||
const fetcher: OpenAPIFetcher = {
|
const fetcher: OpenAPIFetcher = {
|
||||||
fetch: async (url) => {
|
fetch: async (url) => {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
return parseOpenAPI({ value: await response.text(), url });
|
return parseOpenAPI({ value: await response.text(), url, parseMarkdown: async (v) => v });
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { toJSON, fromJSON } from 'flatted';
|
|||||||
import { OpenAPICustomSpecProperties } from './parser';
|
import { OpenAPICustomSpecProperties } from './parser';
|
||||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||||
import { noReference } from './utils';
|
import { noReference } from './utils';
|
||||||
import { traverse } from './parser/traverse';
|
import { parseDescriptions } from './parser/markdown';
|
||||||
import { AnyObject } from '@scalar/openapi-parser';
|
|
||||||
|
|
||||||
export interface OpenAPIFetcher {
|
export interface OpenAPIFetcher {
|
||||||
/**
|
/**
|
||||||
@@ -16,11 +15,6 @@ export interface OpenAPIFetcher {
|
|||||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse markdown to the react element to render.
|
|
||||||
*/
|
|
||||||
parseMarkdown?: (input: string) => Promise<string>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||||
@@ -48,10 +42,8 @@ export async function fetchOpenAPIOperation(
|
|||||||
path: string;
|
path: string;
|
||||||
method: string;
|
method: string;
|
||||||
},
|
},
|
||||||
rawFetcher: OpenAPIFetcher,
|
fetcher: OpenAPIFetcher,
|
||||||
): Promise<OpenAPIOperationData | null> {
|
): Promise<OpenAPIOperationData | null> {
|
||||||
const fetcher = cacheFetcher(rawFetcher);
|
|
||||||
|
|
||||||
const schema = await fetcher.fetch(input.url);
|
const schema = await fetcher.fetch(input.url);
|
||||||
|
|
||||||
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
||||||
@@ -60,12 +52,6 @@ export async function fetchOpenAPIOperation(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse description in markdown
|
|
||||||
const { parseMarkdown } = fetcher;
|
|
||||||
if (parseMarkdown) {
|
|
||||||
operation = await parseDescriptions(operation, parseMarkdown);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve common parameters
|
// Resolve common parameters
|
||||||
const commonParameters = getPathObjectParameter(schema, input.path);
|
const commonParameters = getPathObjectParameter(schema, input.path);
|
||||||
if (commonParameters) {
|
if (commonParameters) {
|
||||||
@@ -103,31 +89,6 @@ export async function fetchOpenAPIOperation(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function parseDescriptions<T extends AnyObject>(
|
|
||||||
spec: T,
|
|
||||||
parseMarkdown: (input: string) => Promise<string>,
|
|
||||||
): Promise<T> {
|
|
||||||
const promises: Record<string, Promise<string>> = {};
|
|
||||||
const results: Record<string, string> = {};
|
|
||||||
traverse(spec, (obj) => {
|
|
||||||
if ('description' in obj && typeof obj.description === 'string') {
|
|
||||||
promises[obj.description] = parseMarkdown(obj.description);
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
});
|
|
||||||
await Promise.all(
|
|
||||||
Object.entries(promises).map(async ([key, promise]) => {
|
|
||||||
results[key] = await promise;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return traverse(spec, (obj) => {
|
|
||||||
if ('description' in obj && typeof obj.description === 'string') {
|
|
||||||
obj.description = results[obj.description];
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
}) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a path object from its path.
|
* Get a path object from its path.
|
||||||
*/
|
*/
|
||||||
@@ -176,20 +137,3 @@ function getOperationByPathAndMethod(
|
|||||||
}
|
}
|
||||||
return pathObject[normalizedMethod];
|
return pathObject[normalizedMethod];
|
||||||
}
|
}
|
||||||
|
|
||||||
function cacheFetcher(fetcher: OpenAPIFetcher): OpenAPIFetcher {
|
|
||||||
const cache = new Map<string, Promise<any>>();
|
|
||||||
|
|
||||||
return {
|
|
||||||
async fetch(url) {
|
|
||||||
if (cache.has(url)) {
|
|
||||||
return cache.get(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = fetcher.fetch(url);
|
|
||||||
cache.set(url, promise);
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
parseMarkdown: fetcher.parseMarkdown,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ import { parseOpenAPIV3 } from './v3';
|
|||||||
* It will also convert Swagger 2.0 to OpenAPI 3.0.
|
* It will also convert Swagger 2.0 to OpenAPI 3.0.
|
||||||
* It can throw an `OpenAPIParseError` if the document is invalid.
|
* It can throw an `OpenAPIParseError` if the document is invalid.
|
||||||
*/
|
*/
|
||||||
export async function parseOpenAPI(input: { value: string; url: string }) {
|
export async function parseOpenAPI(input: {
|
||||||
|
value: string;
|
||||||
|
url: string;
|
||||||
|
parseMarkdown: (input: string) => Promise<string>;
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
return await parseOpenAPIV3(input);
|
return await parseOpenAPIV3(input);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { AnyObject } from '@scalar/openapi-parser';
|
||||||
|
import { traverse } from './traverse';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse descriptions in the spec to markdown.
|
||||||
|
*/
|
||||||
|
export async function parseDescriptions<T extends AnyObject>(input: {
|
||||||
|
specification: T;
|
||||||
|
parseMarkdown: (input: string) => Promise<string>;
|
||||||
|
}): Promise<T> {
|
||||||
|
const { specification, parseMarkdown } = input;
|
||||||
|
const promises: Record<string, Promise<string>> = {};
|
||||||
|
const results: Record<string, string> = {};
|
||||||
|
traverse(specification, (obj, path) => {
|
||||||
|
if (checkHasDescription(obj) && path) {
|
||||||
|
promises[hashPath(path)] = parseMarkdown(obj.description);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(promises).map(async ([key, promise]) => {
|
||||||
|
results[key] = await promise;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return traverse(specification, (obj, path) => {
|
||||||
|
if (
|
||||||
|
checkHasDescription(obj) &&
|
||||||
|
typeof obj.description === 'string' &&
|
||||||
|
path &&
|
||||||
|
results[hashPath(path)]
|
||||||
|
) {
|
||||||
|
obj.description = results[hashPath(path)];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the object contains a description.
|
||||||
|
*/
|
||||||
|
function checkHasDescription(obj: AnyObject): obj is { description: string } {
|
||||||
|
return 'description' in obj && typeof obj.description === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a path.
|
||||||
|
*/
|
||||||
|
function hashPath(path: string[]): string {
|
||||||
|
return path.join('/');
|
||||||
|
}
|
||||||
@@ -13,11 +13,12 @@ import { AnyApiDefinitionFormat } from '@scalar/openapi-parser';
|
|||||||
export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
||||||
value: AnyApiDefinitionFormat;
|
value: AnyApiDefinitionFormat;
|
||||||
url: string;
|
url: string;
|
||||||
|
parseMarkdown: (input: string) => Promise<string>;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||||
> {
|
> {
|
||||||
const { value, url } = input;
|
const { value, url, parseMarkdown } = input;
|
||||||
// In this case we want the raw value to be able to convert it.
|
// In this case we want the raw value to be able to convert it.
|
||||||
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, url }) : value;
|
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, url }) : value;
|
||||||
try {
|
try {
|
||||||
@@ -33,7 +34,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
|||||||
patch: true,
|
patch: true,
|
||||||
})) as ConvertOutputOptions;
|
})) as ConvertOutputOptions;
|
||||||
|
|
||||||
return parseOpenAPIV3({ url, value: convertResult.openapi });
|
return parseOpenAPIV3({ url, value: convertResult.openapi, parseMarkdown });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.name === 'S2OError') {
|
if (error instanceof Error && error.name === 'S2OError') {
|
||||||
throw new OpenAPIParseError(
|
throw new OpenAPIParseError(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { OpenAPICustomSpecProperties } from './types';
|
|||||||
import { AnyApiDefinitionFormat, dereference } from '@scalar/openapi-parser';
|
import { AnyApiDefinitionFormat, dereference } from '@scalar/openapi-parser';
|
||||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||||
import { OpenAPIParseError } from './error';
|
import { OpenAPIParseError } from './error';
|
||||||
|
import { parseDescriptions } from './markdown';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a raw string into an OpenAPI document.
|
* Parse a raw string into an OpenAPI document.
|
||||||
@@ -11,6 +12,7 @@ import { OpenAPIParseError } from './error';
|
|||||||
export async function parseOpenAPIV3(input: {
|
export async function parseOpenAPIV3(input: {
|
||||||
value: AnyApiDefinitionFormat;
|
value: AnyApiDefinitionFormat;
|
||||||
url: string;
|
url: string;
|
||||||
|
parseMarkdown: (input: string) => Promise<string>;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||||
@@ -23,13 +25,20 @@ export async function parseOpenAPIV3(input: {
|
|||||||
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
|
throw new OpenAPIParseError('Invalid OpenAPI document', url, 'invalid-spec');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.version === '2.0') {
|
||||||
|
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = await parseDescriptions({
|
||||||
|
specification: result.schema,
|
||||||
|
parseMarkdown: input.parseMarkdown,
|
||||||
|
});
|
||||||
|
|
||||||
switch (result.version) {
|
switch (result.version) {
|
||||||
case '2.0':
|
|
||||||
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
|
||||||
case '3.0':
|
case '3.0':
|
||||||
return result.schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
return schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||||
case '3.1':
|
case '3.1':
|
||||||
default:
|
default:
|
||||||
return result.schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
return schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user