Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Add `/api/revalidate` endpoint to docs preview server that revalidates all locales by default.
When no `locale` parameter is provided, the endpoint recomputes translations for all configured locales.
When a specific `locale` parameter is provided (e.g., `{"locale": "fr"}`), only that locale is revalidated.
type: feat
35 changes: 34 additions & 1 deletion packages/cli/docs-preview/src/createBunServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export interface BunServerOptions {
* E.g., "/fr/getting-started" -> "fr", "/getting-started" -> undefined
*/
extractLocaleFromPath?(urlPath: string | undefined): string | undefined;
/**
* Callback to revalidate locale translations.
* If locale is undefined, revalidates all locales.
* Returns an array of revalidated locale codes.
*/
onRevalidate?(locale?: string): Promise<string[]>;
}

export interface BunServer {
Expand All @@ -57,7 +63,7 @@ export function createBunServer(options: BunServerOptions): BunServer | undefine
return undefined;
}

const { port, debugLogger, getDocsLoadResponse, extractLocaleFromPath } = options;
const { port, debugLogger, getDocsLoadResponse, extractLocaleFromPath, onRevalidate } = options;

type WsData = { connectionId: string };
const connections = new Map<BunServerWebSocket<WsData>, { pingInterval: NodeJS.Timeout; lastPong: number }>();
Expand Down Expand Up @@ -112,6 +118,33 @@ export function createBunServer(options: BunServerOptions): BunServer | undefine
}
}

// POST /api/revalidate
if (req.method === "POST" && url.pathname === "/api/revalidate") {
if (onRevalidate == null) {
return new Response(JSON.stringify({ error: "Revalidation not supported" }), {
status: 501,
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
});
}
try {
const body = (await req.json()) as { locale?: string } | undefined;
const revalidatedLocales = await onRevalidate(body?.locale);
return new Response(JSON.stringify({ revalidated: revalidatedLocales }), {
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
});
} catch (error) {
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Failed to revalidate locales"
}),
{
status: 500,
headers: { "Content-Type": "application/json", ...CORS_HEADERS }
}
);
}
}

// GET /_local/...
const localMatch = /^\/_local\/(.*)/.exec(url.pathname);
if (req.method === "GET" && localMatch != null) {
Expand Down
61 changes: 60 additions & 1 deletion packages/cli/docs-preview/src/runAppPreviewServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1056,6 +1056,43 @@ export async function runAppPreviewServer({
}
});

// eslint-disable-next-line @typescript-eslint/no-misused-promises
app.post("/api/revalidate", async (req, res) => {
try {
const requestBody = req.body as { locale?: string } | undefined;
const specificLocale = requestBody?.locale;

if (previewResult == null) {
context.logger.warn("Cannot revalidate: no docs definition loaded");
return res.status(503).json({ error: "No docs definition loaded" });
}

// Revalidate all locales by default, or just the specified locale
if (specificLocale != null) {
context.logger.info(`Revalidating locale: ${specificLocale}`);
const singleLocaleMap = await computeTranslatedDefinitions(previewResult);
const revalidatedDefinition = singleLocaleMap.get(specificLocale);
if (revalidatedDefinition != null) {
translatedDefinitions.set(specificLocale, revalidatedDefinition);
return res.json({ revalidated: [specificLocale] });
} else {
return res.status(404).json({ error: `Locale '${specificLocale}' not found` });
}
} else {
context.logger.info("Revalidating all locales");
translatedDefinitions = await computeTranslatedDefinitions(previewResult);
const revalidatedLocales = Array.from(translatedDefinitions.keys());
return res.json({
revalidated: revalidatedLocales.length > 0 ? revalidatedLocales : ["default"]
});
}
} catch (error) {
context.logger.error("Error revalidating locales", (error as Error).message);
context.logger.error("Stack trace:", (error as Error).stack ?? "");
res.status(500).json({ error: "Failed to revalidate locales" });
}
});

app.get(/^\/_local\/(.*)/, (req, res) => {
return res.sendFile(`/${req.params[0]}`);
});
Expand All @@ -1068,7 +1105,29 @@ export async function runAppPreviewServer({
port: backendPort,
debugLogger,
getDocsLoadResponse: buildDocsLoadResponse,
extractLocaleFromPath
extractLocaleFromPath,
onRevalidate: async (locale?: string): Promise<string[]> => {
if (previewResult == null) {
throw new Error("No docs definition loaded");
}

if (locale != null) {
context.logger.info(`Revalidating locale: ${locale}`);
const singleLocaleMap = await computeTranslatedDefinitions(previewResult);
const revalidatedDefinition = singleLocaleMap.get(locale);
if (revalidatedDefinition != null) {
translatedDefinitions.set(locale, revalidatedDefinition);
return [locale];
} else {
throw new Error(`Locale '${locale}' not found`);
}
} else {
context.logger.info("Revalidating all locales");
translatedDefinitions = await computeTranslatedDefinitions(previewResult);
const revalidatedLocales = Array.from(translatedDefinitions.keys());
return revalidatedLocales.length > 0 ? revalidatedLocales : ["default"];
}
}
});
if (bunHandle != null) {
sendData = bunHandle.sendData;
Expand Down
Loading