Files
cfw-autumn/packages/openapi/utils/mintlifyTransform/index.ts
2026-05-01 17:43:23 +01:00

188 lines
5.9 KiB
TypeScript

import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import yaml from "yaml";
import {
type GeneratedWebhookPage,
generateApiReference,
} from "../apiReferenceGenerator/index.js";
import { removeInternalFields } from "../openapiTransform/removeInternalFields.js";
import { transformNode } from "./transformNode.js";
export { stripJsDocTags } from "./stripJsDocTags.js";
export {
transformPythonCodeSample,
transformTypeScriptCodeSample,
} from "./transformCodeSamples.js";
export { resolveSchemaExample, transformNode } from "./transformNode.js";
/**
* Transforms an OpenAPI YAML document for Mintlify consumption.
*
* - Removes internal fields (marked with `internal: true` or `x-internal: true`)
* - Strips JSDoc tags from descriptions
* - Transforms Speakeasy code samples to use autumn-js format
* - Copies schema examples to response content level
*/
export function transformOpenApiForMintlify(yamlContent: string): string {
const doc = yaml.parse(yamlContent) as Record<string, unknown>;
const schemas = (doc.components as Record<string, unknown>)?.schemas as
| Record<string, unknown>
| undefined;
removeInternalFields({ openApiDocument: doc });
transformNode(doc, schemas);
return yaml.stringify(doc);
}
/**
* Generates Mintlify documentation from OpenAPI spec.
*
* 1. Transforms OpenAPI (strips JSDoc tags, fixes code samples)
* 2. Generates API reference MDX files with dynamic parameter fields
* 3. Updates docs.json navigation with webhook groups
*/
export async function generateMintlifyDocs({
openApiPath,
docsDir,
}: {
openApiPath: string;
docsDir: string;
}): Promise<void> {
console.log("Transforming OpenAPI for Mintlify docs...");
const yamlContent = readFileSync(openApiPath, "utf-8");
const transformedYaml = transformOpenApiForMintlify(yamlContent);
writeFileSync(openApiPath, transformedYaml);
console.log("Mintlify transformation complete");
console.log("Generating API reference MDX files...");
const manualMdxDir = path.resolve(docsDir, "../api-reference-generator");
const outputMdxDir = path.resolve(docsDir, "api-reference");
const { webhookPages } = await generateApiReference({
openApiPath,
manualMdxDir,
outputDir: outputMdxDir,
});
console.log("API reference MDX generation complete");
if (webhookPages.length > 0) {
updateDocsJsonWebhooks({ docsDir, webhookPages });
}
}
/**
* Reads the Slack and Discord Svix transform source files from
* `others/svix-transforms/` and writes them as Mintlify snippet MDX files
* (fenced JS code blocks with a tab title) so they can be imported and
* rendered inside a `<CodeGroup>` on the docs page without needing to
* hand-copy the code.
*
* The transform code is the source of truth — running this keeps the
* docs page in sync automatically.
*/
export function syncSvixTransforms({
svixTransformsDir,
docsDir,
}: {
svixTransformsDir: string;
docsDir: string;
}): void {
console.log("Syncing Svix transforms to docs snippets...");
const snippetsDir = path.join(docsDir, "snippets", "svix-transforms");
mkdirSync(snippetsDir, { recursive: true });
const platforms = [
{ source: "slack.ts", snippet: "slack.mdx", title: "Slack" },
{ source: "discord.ts", snippet: "discord.mdx", title: "Discord" },
] as const;
for (const { source, snippet, title } of platforms) {
const sourcePath = path.join(svixTransformsDir, source);
const snippetPath = path.join(snippetsDir, snippet);
const code = readFileSync(sourcePath, "utf-8");
// A Mintlify snippet that renders as a fenced code block. The title
// after the language tag becomes the tab label inside <CodeGroup>.
const content = `{/* AUTO-GENERATED by syncSvixTransforms in packages/openapi/utils/mintlifyTransform/index.ts */}
{/* Source: others/svix-transforms/${source} */}
{/* Edit the source file, not this snippet. */}
\`\`\`js ${title}
${code.trimEnd()}
\`\`\`
`;
writeFileSync(snippetPath, content);
console.log(` Wrote ${path.relative(docsDir, snippetPath)}`);
}
}
/**
* Updates the docs.json navigation to include webhook groups in the
* API Reference tab, replacing any previously generated webhook groups.
*/
function updateDocsJsonWebhooks({
docsDir,
webhookPages,
}: {
docsDir: string;
webhookPages: GeneratedWebhookPage[];
}) {
const docsJsonPath = path.join(docsDir, "docs.json");
const docsJson = JSON.parse(readFileSync(docsJsonPath, "utf-8"));
const tabs = docsJson.navigation?.tabs;
if (!Array.isArray(tabs)) return;
const apiTab = tabs.find(
(tab: Record<string, unknown>) => tab.tab === "API Reference",
);
if (!apiTab?.groups || !Array.isArray(apiTab.groups)) return;
// Build nested subgroups under a single "Webhook Events" group
const groupedPages = new Map<string, string[]>();
for (const page of webhookPages) {
const existing = groupedPages.get(page.group) ?? [];
existing.push(page.pagePath);
groupedPages.set(page.group, existing);
}
const subgroups = [...groupedPages.entries()].map(
([groupName, pages]) => ({
group: groupName,
pages,
}),
);
const webhookEventsGroup = {
group: "Webhook Events",
pages: subgroups,
};
// Remove any existing "Webhook Events" or old "Webhooks:" groups
const filteredGroups = apiTab.groups.filter(
(group: Record<string, unknown>) => {
const name = group.group as string | undefined;
return name && !name.startsWith("Webhooks:") && name !== "Webhook Events";
},
);
// Insert before "Platform (Beta)" if it exists, otherwise at the end
const platformIdx = filteredGroups.findIndex(
(g: Record<string, unknown>) => g.group === "Platform (Beta)",
);
if (platformIdx >= 0) {
filteredGroups.splice(platformIdx, 0, webhookEventsGroup);
} else {
filteredGroups.push(webhookEventsGroup);
}
apiTab.groups = filteredGroups;
writeFileSync(docsJsonPath, `${JSON.stringify(docsJson, null, "\t")}\n`);
console.log(
` Updated docs.json with ${subgroups.length} webhook subgroup(s)`,
);
}