Skip to content

Commit 09e1a6e

Browse files
authored
docs: Fix OpenAPI spec (#11421)
### Description <!-- ✍️ Write a short summary of your work. If necessary, include relevant screenshots. --> ### Testing Instructions <!-- Give a quick description of steps to test your changes. -->
1 parent ddc3cc3 commit 09e1a6e

12 files changed

Lines changed: 413 additions & 654 deletions

File tree

docs/site/app/[lang]/(openapi)/docs/openapi/[[...slug]]/openapi.css

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
@tailwind base;
2-
@tailwind components;
3-
@tailwind utilities;
4-
1+
/* Custom overrides for OpenAPI page button colors */
52
#nd-page
63
> article
74
> div.prose

docs/site/app/[lang]/(openapi)/docs/openapi/[[...slug]]/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
DocsTitle
77
} from "@/components/geistdocs/docs-page";
88
import { getMDXComponents } from "@/components/geistdocs/mdx-components";
9-
import { openapi, openapiPages } from "@/lib/geistdocs/source";
9+
import { APIPage } from "@/components/api-page";
10+
import { openapiPages } from "@/lib/geistdocs/source";
1011
import "./openapi.css";
1112

1213
const Page = async ({
@@ -31,7 +32,7 @@ const Page = async ({
3132
<MDX
3233
components={getMDXComponents({
3334
components: {
34-
APIPage: openapi.APIPage
35+
APIPage
3536
}
3637
})}
3738
/>
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import json from "../../../.openapi.json";
1+
import { fetchOpenAPISpec } from "@/lib/openapi-spec";
22

33
export const revalidate = 0;
44

5-
export function GET(): Response {
6-
return Response.json(json);
5+
export async function GET(): Promise<Response> {
6+
const spec = await fetchOpenAPISpec();
7+
return Response.json(spec);
78
}

docs/site/app/styles/geistdocs.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@import "tailwindcss";
22
@import "fumadocs-ui/css/shadcn.css";
33
@import "fumadocs-ui/css/preset.css";
4+
@import "fumadocs-openapi/css/preset.css";
45
@import "tw-animate-css";
56
@import "./design-system.css";
67

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"use client";
2+
import { defineClientConfig } from "fumadocs-openapi/ui/client";
3+
4+
export default defineClientConfig({});

docs/site/components/api-page.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { openapi } from "@/lib/openapi";
2+
import { createAPIPage } from "fumadocs-openapi/ui";
3+
import client from "./api-page.client";
4+
5+
export const APIPage = createAPIPage(openapi, {
6+
client
7+
});

docs/site/lib/geistdocs/source.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
loader
77
} from "fumadocs-core/source";
88
import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons";
9-
import { createOpenAPI } from "fumadocs-openapi/server";
9+
import { openapiPlugin } from "fumadocs-openapi/server";
1010
import {
1111
docs,
1212
blogDocs,
@@ -95,11 +95,10 @@ export const externalBlog = loader({
9595
// OpenAPI loaders
9696
export const openapiPages = loader({
9797
baseUrl: "/docs/openapi",
98-
source: createSource(openapiDocs, openapiMeta)
98+
source: createSource(openapiDocs, openapiMeta),
99+
plugins: [openapiPlugin()]
99100
});
100101

101-
export const openapi = createOpenAPI();
102-
103102
// Extra pages (terms, governance, etc.)
104103
export const extraPages = loader({
105104
baseUrl: "/",

docs/site/lib/openapi-spec.ts

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// The OpenAPI spec for a self-hosted implementation is generated
2+
// from the Vercel Remote Cache implementation.
3+
// The Vercel Remote Cache spec is more specific to the needs of Vercel
4+
// while the self-hosted spec is more open for anyone to implement.
5+
//
6+
// While the two specifications are related enough to use the Vercel Remote Cache
7+
// as the source of truth, the self-hosted Remote Cache has all
8+
// of the same capabilities. Because of this,
9+
// we do some light processing to make sure that the content
10+
// in the self-hosted spec makes sense for self-hosted users.
11+
//
12+
// You can verify differences for the specs by comparing:
13+
// Vercel Remote Cache: https://vercel.com/docs/rest-api/reference/endpoints/artifacts/record-an-artifacts-cache-usage-event
14+
// Self-hosted: https://turborepo.com/api/remote-cache-spec
15+
16+
import type { Document } from "fumadocs-openapi";
17+
18+
interface OpenAPISpec {
19+
paths?: Record<
20+
string,
21+
Record<
22+
string,
23+
{
24+
responses?: Record<
25+
string,
26+
{
27+
description?: string;
28+
headers?: Record<
29+
string,
30+
{
31+
schema: {
32+
type: string;
33+
};
34+
description: string;
35+
}
36+
>;
37+
}
38+
>;
39+
}
40+
>
41+
>;
42+
servers?: Array<{
43+
url: string;
44+
description?: string;
45+
}>;
46+
}
47+
48+
// Define a more specific type for the OpenAPI value structure
49+
type OpenAPIValue =
50+
| string
51+
| number
52+
| boolean
53+
| null
54+
| { [key: string]: OpenAPIValue }
55+
| Array<OpenAPIValue>;
56+
57+
/* The Vercel Remote Cache spec has examples that show Vercel values.
58+
* Removing them makes the self-hosted spec easier to use. */
59+
const removeExamples = (obj: OpenAPIValue): OpenAPIValue => {
60+
if (!obj || typeof obj !== "object") return obj;
61+
62+
if (Array.isArray(obj)) {
63+
return obj.map((item) => removeExamples(item));
64+
}
65+
66+
const result: Record<string, OpenAPIValue> = {};
67+
for (const [key, value] of Object.entries(
68+
obj as Record<string, OpenAPIValue>
69+
)) {
70+
if (key !== "example") {
71+
result[key] = removeExamples(value);
72+
}
73+
}
74+
75+
return result;
76+
};
77+
78+
/* The Vercel Remote Cache spec has responses related to billing.
79+
* Self-hosted users don't need these. */
80+
function removeBillingRelated403Responses(spec: OpenAPISpec): OpenAPISpec {
81+
// Define billing-related phrases to filter out
82+
const billingPhrases = [
83+
"The customer has reached their spend cap limit and has been paused",
84+
"The Remote Caching usage limit has been reached for this account",
85+
"Remote Caching has been disabled for this team or user"
86+
];
87+
88+
// Process all paths
89+
for (const path in spec.paths) {
90+
const pathObj = spec.paths[path];
91+
92+
// Process all methods in each path
93+
for (const method in pathObj) {
94+
const methodObj = pathObj[method];
95+
96+
// Check if the method has responses
97+
if (methodObj.responses?.["403"]) {
98+
const description = methodObj.responses["403"].description;
99+
100+
// Split the description by newlines
101+
const descriptionLines = description?.split("\n") ?? [];
102+
103+
// Filter out billing-related lines
104+
const filteredLines = descriptionLines.filter((line) => {
105+
return !billingPhrases.some((phrase) => line.includes(phrase));
106+
});
107+
108+
// If there are remaining lines, join them back together
109+
if (filteredLines.length > 0) {
110+
methodObj.responses["403"].description = filteredLines.join("\n");
111+
} else {
112+
// If all lines were billing-related, set a generic permission message
113+
methodObj.responses["403"].description =
114+
"You do not have permission to access this resource.";
115+
}
116+
}
117+
}
118+
}
119+
120+
return spec;
121+
}
122+
123+
/* Add x-artifact-tag header to artifact download endpoint response */
124+
function addArtifactTagHeader(spec: OpenAPISpec): OpenAPISpec {
125+
// Target only the specific /v8/artifacts/{hash} endpoint
126+
const artifactEndpoint = "/v8/artifacts/{hash}";
127+
128+
if (spec.paths?.[artifactEndpoint]) {
129+
// Get the GET method for this endpoint
130+
const getMethod = spec.paths[artifactEndpoint].get;
131+
132+
if (getMethod.responses?.["200"]) {
133+
const response = getMethod.responses["200"];
134+
135+
// Add headers to the response if they don't exist
136+
if (!response.headers) {
137+
response.headers = {};
138+
}
139+
140+
// Add the x-artifact-tag header
141+
response.headers["x-artifact-tag"] = {
142+
schema: {
143+
type: "string"
144+
},
145+
description: "The signature of the artifact found"
146+
};
147+
}
148+
}
149+
150+
return spec;
151+
}
152+
153+
const updateServerDescription = (spec: OpenAPISpec): OpenAPISpec => {
154+
if (spec.servers && spec.servers.length > 0) {
155+
const serverIndex = spec.servers.findIndex(
156+
(server) => server.url === "https://api.vercel.com"
157+
);
158+
159+
if (serverIndex !== -1) {
160+
spec.servers[serverIndex].description =
161+
"Vercel Remote Cache implementation for reference.";
162+
}
163+
return spec;
164+
}
165+
return spec;
166+
};
167+
168+
export const OPENAPI_SPEC_URL = "https://turborepo.com/api/remote-cache-spec";
169+
170+
/**
171+
* Fetches and transforms the OpenAPI spec from the remote URL.
172+
* Applies transformations to make it suitable for self-hosted documentation.
173+
*/
174+
export async function fetchOpenAPISpec(): Promise<Document> {
175+
const spec = await fetch(OPENAPI_SPEC_URL)
176+
.then((res) => res.json())
177+
.then(
178+
(json: unknown) => removeExamples(json as OpenAPIValue) as OpenAPISpec
179+
)
180+
.then((json) => removeBillingRelated403Responses(json))
181+
.then((json) => addArtifactTagHeader(json))
182+
.then((json) => updateServerDescription(json));
183+
184+
return spec as Document;
185+
}

docs/site/lib/openapi.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createOpenAPI } from "fumadocs-openapi/server";
2+
// @ts-expect-error - Using .ts extension for node --experimental-strip-types in generate script
3+
import { fetchOpenAPISpec } from "./openapi-spec.ts";
4+
5+
export const openapi = createOpenAPI({
6+
// Use a function to provide the transformed spec dynamically
7+
input: async () => {
8+
const spec = await fetchOpenAPISpec();
9+
return {
10+
"remote-cache": spec
11+
};
12+
}
13+
});

docs/site/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"flags": "^4.0.2",
3939
"fumadocs-core": "16.2.2",
4040
"fumadocs-mdx": "14.0.4",
41-
"fumadocs-openapi": "^7.0.1",
41+
"fumadocs-openapi": "^10.2.4",
4242
"fumadocs-ui": "16.2.2",
4343
"input-otp": "^1.4.2",
4444
"jotai": "^2.15.2",

0 commit comments

Comments
 (0)