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;
}
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({
const { data, specUrl, publicURL, error } = await resolveOpenAPIOperationBlock({
block,
context: context.contentContext,
});
@@ -46,7 +46,11 @@ async function OpenAPIOperationBody(props: BlockProps<AnyOpenAPIOperationsBlock>
return (
<BaseOpenAPIOperation
data={data}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
className="openapi-block"
/>
);
@@ -25,7 +25,7 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
return null;
}
const { data, specUrl, error } = await resolveOpenAPISchemasBlock({
const { data, specUrl, publicURL, error } = await resolveOpenAPISchemasBlock({
block,
context: context.contentContext,
});
@@ -48,7 +48,11 @@ async function OpenAPISchemasBody(props: BlockProps<OpenAPISchemasBlock>) {
<BaseOpenAPISchemas
schemas={data.schemas}
grouped={block.data.grouped}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
className="openapi-block"
/>
);
@@ -26,7 +26,7 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return null;
}
const { data, specUrl, error } = await resolveOpenAPIWebhookBlock({
const { data, specUrl, publicURL, error } = await resolveOpenAPIWebhookBlock({
block,
context: context.contentContext,
});
@@ -48,7 +48,11 @@ async function OpenAPIWebhookBody(props: BlockProps<OpenAPIWebhookBlock>) {
return (
<BaseOpenAPIWebhook
data={data}
context={getOpenAPIContext({ props, specUrl, context: context.contentContext })}
context={getOpenAPIContext({
props,
specUrl: publicURL,
context: context.contentContext,
})}
className="openapi-block"
/>
);
@@ -24,7 +24,7 @@ import type {
*/
export function getOpenAPIContext(args: {
props: BlockProps<AnyOpenAPIOperationsBlock | OpenAPISchemasBlock | OpenAPIWebhookBlock>;
specUrl: string;
specUrl: string | null;
context: GitBookAnyContext | undefined;
}): OpenAPIContextInput {
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;
if (!resolved) {
return { filesystem: null, specUrl: null };
return { filesystem: null, specUrl: null, publicURL: null };
}
const result = await (() => {
// If the reference is a new OpenAPI reference, we return it.
if (ref.kind === 'openapi') {
assert(resolved.openAPIFilesystem);
return resolved.openAPIFilesystem;
assert(resolved.openapi?.filesystem);
return resolved.openapi.filesystem;
}
// For legacy blocks ("swagger"), we need to fetch the file system.
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 });
}
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;
if (!block.data.path || !block.data.method) {
return { data: null, specUrl: null };
return { data: null, specUrl: null, publicURL: null };
}
try {
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem) {
return { data: null, specUrl: null };
return { data: null, specUrl: null, publicURL: null };
}
const data = await resolveOpenAPIOperation(filesystem, {
@@ -52,7 +52,7 @@ async function resolveOpenAPIOperationBlockNoCache(
method: block.data.method,
});
return { data, specUrl };
return { data, specUrl, publicURL };
} catch (error) {
if (error instanceof OpenAPIParseError) {
return { error };
@@ -37,21 +37,21 @@ async function baseResolveOpenAPISchemasBlock(
): Promise<ResolveOpenAPISchemasBlockResult> {
const { context, block } = args;
if (!block.data.schemas || !block.data.schemas.length) {
return { data: null, specUrl: null };
return { data: null, specUrl: null, publicURL: null };
}
try {
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem || !specUrl) {
return { data: null, specUrl: null };
return { data: null, specUrl: null, publicURL: null };
}
const data = await resolveOpenAPISchemas(filesystem, {
schemas: block.data.schemas,
});
return { data, specUrl };
return { data, specUrl, publicURL };
} catch (error) {
if (error instanceof OpenAPIParseError) {
return { error };
@@ -35,14 +35,14 @@ async function baseResolveOpenAPIWebhookBlock(
): Promise<ResolveOpenAPIWebhookBlockResult> {
const { context, block } = args;
if (!block.data.name || !block.data.method) {
return { data: null, specUrl: null };
return { data: null, specUrl: null, publicURL: null };
}
try {
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
const { filesystem, specUrl, publicURL } = await fetchOpenAPIFilesystem({ block, context });
if (!filesystem) {
return { data: null, specUrl: null };
return { data: null, specUrl: null, publicURL: null };
}
const data = await resolveOpenAPIWebhook(filesystem, {
@@ -50,7 +50,7 @@ async function baseResolveOpenAPIWebhookBlock(
method: block.data.method,
});
return { data, specUrl };
return { data, specUrl, publicURL };
} catch (error) {
if (error instanceof OpenAPIParseError) {
return { error };
+5 -1
View File
@@ -38,6 +38,8 @@ export type FetchOpenAPIFilesystemResult =
error?: undefined;
filesystem: Filesystem<OpenAPIV3xDocument> | 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;
@@ -48,13 +50,14 @@ type FetchOpenAPIFilesystemError = {
error: OpenAPIParseError;
filesystem?: undefined;
specUrl?: undefined;
publicURL?: undefined;
};
/**
* Resolved OpenAPI block result.
*/
export type ResolveOpenAPIBlockResult<T> =
| { error?: undefined; data: T | null; specUrl: string | null }
| { error?: undefined; data: T | null; specUrl: string | null; publicURL: string | null }
| ResolveOpenAPIBlockError;
/**
@@ -64,4 +67,5 @@ type ResolveOpenAPIBlockError = {
error: OpenAPIParseError;
data?: undefined;
specUrl?: undefined;
publicURL?: undefined;
};
+12 -7
View File
@@ -58,12 +58,17 @@ export interface ResolvedContentRef {
context: GitBookSpaceContext;
revisionReusableContent: RevisionReusableContent;
};
/** Resolve OpenAPI spec filesystem. */
openAPIFilesystem?: Filesystem;
/**
* Space that the content ref belongs to (if applicable).
*/
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 {
@@ -340,13 +345,13 @@ export async function resolveContentRef(
return null;
}
return {
// @ts-expect-error - Backward compatibility: `urls.source` replaces `url` in the next API version
href: openAPISpecVersionContent.urls?.source ?? openAPISpecVersionContent.url,
href: openAPISpecVersionContent.urls.source,
text: contentRef.spec,
active: false,
openAPIFilesystem: openAPISpecVersionContent.filesystem as Filesystem,
// @ts-expect-error - Public URL is not yet supported in the API
publicURL: openAPISpecVersionContent.urls?.public,
openapi: {
filesystem: openAPISpecVersionContent.filesystem as Filesystem,
publicURL: openAPISpecVersionContent.urls.public,
},
};
}
@@ -245,7 +245,7 @@ function OpenAPICodeSampleFooter(props: {
) : (
<span />
)}
{!hideTryItPanel && hasValidHost && (
{!hideTryItPanel && hasValidHost && specUrl && (
<ScalarApiButton
context={resolveScalarClientContext(context, servers, specUrl)}
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;
/**
* 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.