mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +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.
|
||||
*/
|
||||
export async function OpenAPI(props: BlockProps<DocumentBlockOpenAPI>) {
|
||||
const { block, style } = props;
|
||||
const { style } = props;
|
||||
return (
|
||||
<div className={tcls('w-full', 'flex', 'flex-row', style, 'max-w-full')}>
|
||||
<React.Suspense fallback={<OpenAPIFallback />}>
|
||||
<OpenAPIBody {...props} />
|
||||
</React.Suspense>
|
||||
<OpenAPIBody {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@ import { unified } from 'unified';
|
||||
* Parse markdown and output HTML.
|
||||
*/
|
||||
export async function parseMarkdown(markdown: string): Promise<string> {
|
||||
const file = await unified()
|
||||
const promise = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeSanitize)
|
||||
.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 = {
|
||||
fetch: cache({
|
||||
name: 'openapi.fetch.v2',
|
||||
name: 'openapi.fetch.v3',
|
||||
get: async (url: string, options: CacheFunctionOptions) => {
|
||||
// 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:
|
||||
@@ -66,12 +66,11 @@ const fetcher: OpenAPIFetcher = {
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
const data = await parseOpenAPI({ url, value: text });
|
||||
const data = await parseOpenAPI({ url, value: text, parseMarkdown });
|
||||
return {
|
||||
...parseCacheResponse(response),
|
||||
data,
|
||||
};
|
||||
},
|
||||
}),
|
||||
parseMarkdown,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ApiClientModalProvider } from '@scalar/api-client-react';
|
||||
|
||||
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
|
||||
import { OpenAPIOperationData } from './fetchOpenAPIOperation';
|
||||
import { Markdown } from './Markdown';
|
||||
import { OpenAPICodeSample } from './OpenAPICodeSample';
|
||||
import { OpenAPIResponseExample } from './OpenAPIResponseExample';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { parseOpenAPI } from './parser';
|
||||
const fetcher: OpenAPIFetcher = {
|
||||
fetch: async (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 { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { noReference } from './utils';
|
||||
import { traverse } from './parser/traverse';
|
||||
import { AnyObject } from '@scalar/openapi-parser';
|
||||
import { parseDescriptions } from './parser/markdown';
|
||||
|
||||
export interface OpenAPIFetcher {
|
||||
/**
|
||||
@@ -16,11 +15,6 @@ export interface OpenAPIFetcher {
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Parse markdown to the react element to render.
|
||||
*/
|
||||
parseMarkdown?: (input: string) => Promise<string>;
|
||||
}
|
||||
|
||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
@@ -48,10 +42,8 @@ export async function fetchOpenAPIOperation(
|
||||
path: string;
|
||||
method: string;
|
||||
},
|
||||
rawFetcher: OpenAPIFetcher,
|
||||
fetcher: OpenAPIFetcher,
|
||||
): Promise<OpenAPIOperationData | null> {
|
||||
const fetcher = cacheFetcher(rawFetcher);
|
||||
|
||||
const schema = await fetcher.fetch(input.url);
|
||||
|
||||
let operation = getOperationByPathAndMethod(schema, input.path, input.method);
|
||||
@@ -60,12 +52,6 @@ export async function fetchOpenAPIOperation(
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse description in markdown
|
||||
const { parseMarkdown } = fetcher;
|
||||
if (parseMarkdown) {
|
||||
operation = await parseDescriptions(operation, parseMarkdown);
|
||||
}
|
||||
|
||||
// Resolve common parameters
|
||||
const commonParameters = getPathObjectParameter(schema, input.path);
|
||||
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.
|
||||
*/
|
||||
@@ -176,20 +137,3 @@ function getOperationByPathAndMethod(
|
||||
}
|
||||
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 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 {
|
||||
return await parseOpenAPIV3(input);
|
||||
} 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: {
|
||||
value: AnyApiDefinitionFormat;
|
||||
url: string;
|
||||
parseMarkdown: (input: string) => Promise<string>;
|
||||
}): Promise<
|
||||
| OpenAPIV3_1.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.
|
||||
const schema = typeof value === 'string' ? rawParseOpenAPI({ value, url }) : value;
|
||||
try {
|
||||
@@ -33,7 +34,7 @@ export async function convertOpenAPIV2ToOpenAPIV3(input: {
|
||||
patch: true,
|
||||
})) as ConvertOutputOptions;
|
||||
|
||||
return parseOpenAPIV3({ url, value: convertResult.openapi });
|
||||
return parseOpenAPIV3({ url, value: convertResult.openapi, parseMarkdown });
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === 'S2OError') {
|
||||
throw new OpenAPIParseError(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { OpenAPICustomSpecProperties } from './types';
|
||||
import { AnyApiDefinitionFormat, dereference } from '@scalar/openapi-parser';
|
||||
import { OpenAPIV3, OpenAPIV3_1 } from '@scalar/openapi-types';
|
||||
import { OpenAPIParseError } from './error';
|
||||
import { parseDescriptions } from './markdown';
|
||||
|
||||
/**
|
||||
* Parse a raw string into an OpenAPI document.
|
||||
@@ -11,6 +12,7 @@ import { OpenAPIParseError } from './error';
|
||||
export async function parseOpenAPIV3(input: {
|
||||
value: AnyApiDefinitionFormat;
|
||||
url: string;
|
||||
parseMarkdown: (input: string) => Promise<string>;
|
||||
}): Promise<
|
||||
| OpenAPIV3.Document<OpenAPICustomSpecProperties>
|
||||
| OpenAPIV3_1.Document<OpenAPICustomSpecProperties>
|
||||
@@ -23,13 +25,20 @@ export async function parseOpenAPIV3(input: {
|
||||
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) {
|
||||
case '2.0':
|
||||
throw new OpenAPIParseError('Only OpenAPI v3 is supported', url, 'v2-spec');
|
||||
case '3.0':
|
||||
return result.schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||
return schema as OpenAPIV3.Document<OpenAPICustomSpecProperties>;
|
||||
case '3.1':
|
||||
default:
|
||||
return result.schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||
return schema as OpenAPIV3_1.Document<OpenAPICustomSpecProperties>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user