Use OpenAPI publicURL as Scalar specUrl (#4093)

This commit is contained in:
Nolann B.
2026-03-11 11:21:14 +01:00
committed by GitHub
parent b3875a19d7
commit bdc6cd9aba
13 changed files with 76 additions and 34 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Use OpenAPI publicURL as Scalar specUrl
@@ -26,7 +26,7 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return null; return null;
} }
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({ const { data, specUrl, publicURL, error } = await resolveOpenAPIOperationBlock({
block, block,
context: context.contentContext, context: context.contentContext,
}); });
@@ -46,7 +46,11 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return ( return (
<BaseOpenAPIOperation <BaseOpenAPIOperation
data={data} data={data}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })} context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
className="openapi-block" className="openapi-block"
/> />
); );
@@ -25,7 +25,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return null; return null;
} }
const { data, specUrl, error } = await resolveOpenAPISchemasBlock({ const { data, specUrl, publicURL, error } = await resolveOpenAPISchemasBlock({
block, block,
context: context.contentContext, context: context.contentContext,
}); });
@@ -48,7 +48,11 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
<BaseOpenAPISchemas <BaseOpenAPISchemas
schemas={data.schemas} schemas={data.schemas}
grouped={block.data.grouped} grouped={block.data.grouped}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })} context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
className="openapi-block" className="openapi-block"
/> />
); );
@@ -26,7 +26,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return null; return null;
} }
const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({ const { data, specUrl, publicURL, error } = await resolveOpenAPIWebhookBlock({
block, block,
context: context.contentContext, context: context.contentContext,
}); });
@@ -48,7 +48,11 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return ( return (
<BaseOpenAPIWebhook <BaseOpenAPIWebhook
data={data} data={data}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })} context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
className="openapi-block" className="openapi-block"
/> />
); );
@@ -24,7 +24,7 @@ import type {
*/ */
export function getOpenAPIContext(args: { export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>; props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
specUrl: string; specUrl: string | null;
context: GitBookAnyContext | undefined; context: GitBookAnyContext | undefined;
}): OpenAPIContextInput { }): OpenAPIContextInput {
const { props, specUrl, context } = args; const { props, specUrl, context } = args;
+18 -4
View File
@@ -33,14 +33,14 @@ export async function fetchOpenAPIFilesystem(
const resolved = ref ? await resolveContentRef(ref, context) : null; const resolved = ref ? await resolveContentRef(ref, context) : null;
if (!resolved) { if (!resolved) {
return { filesystem: null, specUrl: null }; return { filesystem: null, specUrl: null, publicURL: null };
} }
const result = await (() => { const result = await (() => {
// If the reference is a new OpenAPI reference, we return it. // If the reference is a new OpenAPI reference, we return it.
if (ref.kind === 'openapi') { if (ref.kind === 'openapi') {
assert(resolved.openAPIFilesystem); assert(resolved.openapi?.filesystem);
return resolved.openAPIFilesystem; return resolved.openapi.filesystem;
} }
// For legacy blocks ("swagger"), we need to fetch the file system. // For legacy blocks ("swagger"), we need to fetch the file system.
return fetchFilesystem(resolved.href, context.space.id); return fetchFilesystem(resolved.href, context.space.id);
@@ -50,7 +50,21 @@ export async function fetchOpenAPIFilesystem(
throw new OpenAPIParseError(result.error.message, { code: result.error.code }); throw new OpenAPIParseError(result.error.message, { code: result.error.code });
} }
return { filesystem: result, specUrl: resolved.href }; const publicURL = (() => {
// For new OpenAPI refs, use the explicit publicURL (null when spec is private).
if (ref.kind === 'openapi' && resolved.openapi) {
return resolved.openapi.publicURL;
}
// For legacy "swagger" refs, the href is the public spec URL itself.
return resolved.href;
})();
return {
filesystem: result,
specUrl: resolved.href,
publicURL,
};
} }
/** /**
@@ -37,14 +37,14 @@ async function resolveOpenAPIOperationBlockNoCache(
const { context, block } = args; const { context, block } = args;
if (!block.data.path || !block.data.method) { if (!block.data.path || !block.data.method) {
return { data: null, specUrl: null }; return { data: null, specUrl: null, publicURL: null };
} }
try { try {
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem) { if (!filesystem) {
return { data: null, specUrl: null }; return { data: null, specUrl: null, publicURL: null };
} }
const data = await resolveOpenAPIOperation(filesystem, { const data = await resolveOpenAPIOperation(filesystem, {
@@ -52,7 +52,7 @@ async function resolveOpenAPIOperationBlockNoCache(
method: block.data.method, method: block.data.method,
}); });
return { data, specUrl }; return { data, specUrl, publicURL };
} catch (error) { } catch (error) {
if (error instanceof OpenAPIParseError) { if (error instanceof OpenAPIParseError) {
return { error }; return { error };
@@ -37,21 +37,21 @@ async function baseResolveOpenAPISchemasBlock(
): Promise<ResolveOpenAPISchemasBlockResult> { ): Promise<ResolveOpenAPISchemasBlockResult> {
const { context, block } = args; const { context, block } = args;
if (!block.data.schemas || !block.data.schemas.length) { if (!block.data.schemas || !block.data.schemas.length) {
return { data: null, specUrl: null }; return { data: null, specUrl: null, publicURL: null };
} }
try { try {
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem || !specUrl) { if (!filesystem || !specUrl) {
return { data: null, specUrl: null }; return { data: null, specUrl: null, publicURL: null };
} }
const data = await resolveOpenAPISchemas(filesystem, { const data = await resolveOpenAPISchemas(filesystem, {
schemas: block.data.schemas, schemas: block.data.schemas,
}); });
return { data, specUrl }; return { data, specUrl, publicURL };
} catch (error) { } catch (error) {
if (error instanceof OpenAPIParseError) { if (error instanceof OpenAPIParseError) {
return { error }; return { error };
@@ -35,14 +35,14 @@ async function baseResolveOpenAPIWebhookBlock(
): Promise<ResolveOpenAPIWebhookBlockResult> { ): Promise<ResolveOpenAPIWebhookBlockResult> {
const { context, block } = args; const { context, block } = args;
if (!block.data.name || !block.data.method) { if (!block.data.name || !block.data.method) {
return { data: null, specUrl: null }; return { data: null, specUrl: null, publicURL: null };
} }
try { try {
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context }); const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem) { if (!filesystem) {
return { data: null, specUrl: null }; return { data: null, specUrl: null, publicURL: null };
} }
const data = await resolveOpenAPIWebhook(filesystem, { const data = await resolveOpenAPIWebhook(filesystem, {
@@ -50,7 +50,7 @@ async function baseResolveOpenAPIWebhookBlock(
method: block.data.method, method: block.data.method,
}); });
return { data, specUrl }; return { data, specUrl, publicURL };
} catch (error) { } catch (error) {
if (error instanceof OpenAPIParseError) { if (error instanceof OpenAPIParseError) {
return { error }; return { error };
+5 -1
View File
@@ -38,6 +38,8 @@ export type FetchOpenAPIFilesystemResult =
error?: undefined; error?: undefined;
filesystem: Filesystem<OpenAPIV3xDocument> | null; filesystem: Filesystem<OpenAPIV3xDocument> | null;
specUrl: string | null; specUrl: string | null;
/** Public URL of the OpenAPI spec, used as the spec URL for Scalar's "Test it" modal */
publicURL: string | null;
} }
| FetchOpenAPIFilesystemError; | FetchOpenAPIFilesystemError;
@@ -48,13 +50,14 @@ type FetchOpenAPIFilesystemError = {
error: OpenAPIParseError; error: OpenAPIParseError;
filesystem?: undefined; filesystem?: undefined;
specUrl?: undefined; specUrl?: undefined;
publicURL?: undefined;
}; };
/** /**
* Resolved OpenAPI block result. * Resolved OpenAPI block result.
*/ */
export type ResolveOpenAPIBlockResult<T> = export type ResolveOpenAPIBlockResult<T> =
| { error?: undefined; data: T | null; specUrl: string | null } | { error?: undefined; data: T | null; specUrl: string | null; publicURL: string | null }
| ResolveOpenAPIBlockError; | ResolveOpenAPIBlockError;
/** /**
@@ -64,4 +67,5 @@ type ResolveOpenAPIBlockError = {
error: OpenAPIParseError; error: OpenAPIParseError;
data?: undefined; data?: undefined;
specUrl?: undefined; specUrl?: undefined;
publicURL?: undefined;
}; };
+12 -7
View File
@@ -58,12 +58,17 @@ export interface ResolvedContentRef {
context: GitBookSpaceContext; context: GitBookSpaceContext;
revisionReusableContent: RevisionReusableContent; revisionReusableContent: RevisionReusableContent;
}; };
/** Resolve OpenAPI spec filesystem. */
openAPIFilesystem?: Filesystem;
/** /**
* Space that the content ref belongs to (if applicable). * Space that the content ref belongs to (if applicable).
*/ */
space?: Space; space?: Space;
/** Resolved OpenAPI spec, if the reference is an OpenAPI spec. */
openapi?: {
/** OpenAPI spec filesystem. */
filesystem: Filesystem;
/** Public URL of the OpenAPI spec */
publicURL: string | null;
};
} }
export interface ResolveContentRefOptions { export interface ResolveContentRefOptions {
@@ -340,13 +345,13 @@ export async function resolveContentRef(
return null; return null;
} }
return { return {
// @ts-expect-error - Backward compatibility: `urls.source` replaces `url` in the next API version href: openAPISpecVersionContent.urls.source,
href: openAPISpecVersionContent.urls?.source ?? openAPISpecVersionContent.url,
text: contentRef.spec, text: contentRef.spec,
active: false, active: false,
openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem, openapi: {
// @ts-expect-error - Public URL is not yet supported in the API filesystem: openAPISpecVersionContent.filesystem as Filesystem,
publicURL: openAPISpecVersionContent.urls?.public, publicURL: openAPISpecVersionContent.urls.public,
},
}; };
} }
@@ -245,7 +245,7 @@ function OpenAPICodeSampleFooter(props: {
) : ( ) : (
<span /> <span />
)} )}
{!hideTryItPanel && hasValidHost && ( {!hideTryItPanel && hasValidHost && specUrl && (
<ScalarApiButton <ScalarApiButton
context={resolveScalarClientContext(context, servers, specUrl)} context={resolveScalarClientContext(context, servers, specUrl)}
withProxy={Boolean(data.operation['x-enable-proxy'] ?? data['x-enable-proxy'])} withProxy={Boolean(data.operation['x-enable-proxy'] ?? data['x-enable-proxy'])}
+3 -2
View File
@@ -67,9 +67,10 @@ export interface OpenAPIContext
renderDocument: (props: { document: object }) => React.ReactNode; renderDocument: (props: { document: object }) => React.ReactNode;
/** /**
* Specification URL. * Public specification URL, used by Scalar's "Test it" modal.
* When null, the "Test it" button is hidden.
*/ */
specUrl: string; specUrl: string | null;
/** /**
* Build a signed proxy URL that restricts the proxy to specific origins. * Build a signed proxy URL that restricts the proxy to specific origins.