From 48c7a136a1aa314b1b89fa44dc68237b6ac214fc Mon Sep 17 00:00:00 2001 From: Adam Friedmann Date: Tue, 25 Nov 2025 15:00:26 +0200 Subject: [PATCH] working sdk docs setup --- .gitignore | 5 +- package.json | 9 +- .../category-map.json | 4 + .../file-processing/docs-json-template.json | 62 ++ .../file-processing/file-processing.js | 289 ++++++ .../file-processing/styling.css | 107 +++ .../push-to-docs-repo.js | 183 ++++ .../typedoc-mintlify-content.js | 124 +++ .../typedoc-mintlify-linked-types.js | 383 ++++++++ .../typedoc-mintlify-parameters.js | 550 ++++++++++++ .../typedoc-plugin/typedoc-mintlify-plugin.js | 189 ++++ .../typedoc-mintlify-returns.js | 819 ++++++++++++++++++ .../typedoc-plugin/typedoc-mintlify-utils.js | 12 + .../types-to-expose.json | 12 + typedoc.json | 37 + writing-docs.md | 34 + 16 files changed, 2817 insertions(+), 2 deletions(-) create mode 100644 scripts/mintlify-post-processing/category-map.json create mode 100644 scripts/mintlify-post-processing/file-processing/docs-json-template.json create mode 100755 scripts/mintlify-post-processing/file-processing/file-processing.js create mode 100644 scripts/mintlify-post-processing/file-processing/styling.css create mode 100644 scripts/mintlify-post-processing/push-to-docs-repo.js create mode 100644 scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js create mode 100644 scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-linked-types.js create mode 100644 scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-parameters.js create mode 100644 scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js create mode 100644 scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js create mode 100644 scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-utils.js create mode 100644 scripts/mintlify-post-processing/types-to-expose.json create mode 100644 typedoc.json create mode 100644 writing-docs.md diff --git a/.gitignore b/.gitignore index f655c8d..c952826 100644 --- a/.gitignore +++ b/.gitignore @@ -56,4 +56,7 @@ logs *.tsbuildinfo # Optional REPL history -.node_repl_history \ No newline at end of file +.node_repl_history + +# Docs +docs/ \ No newline at end of file diff --git a/package.json b/package.json index 2b6cee7..0143e22 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,12 @@ "test:e2e": "vitest run tests/e2e", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "prepublishOnly": "npm run build" + "docs": "typedoc", + "prepublishOnly": "npm run build", + "create-docs": "npm run create-docs:generate && npm run create-docs:process", + "push-docs": "node scripts/mintlify-post-processing/push-to-docs-repo.js", + "create-docs:generate": "typedoc", + "create-docs:process": "node scripts/mintlify-post-processing/file-processing/file-processing.js" }, "dependencies": { "axios": "^1.6.2", @@ -30,6 +35,8 @@ "dotenv": "^16.3.1", "eslint": "^8.54.0", "nock": "^13.4.0", + "typedoc": "^0.28.14", + "typedoc-plugin-markdown": "^4.9.0", "typescript": "^5.3.2", "vitest": "^1.0.0" }, diff --git a/scripts/mintlify-post-processing/category-map.json b/scripts/mintlify-post-processing/category-map.json new file mode 100644 index 0000000..c887ed1 --- /dev/null +++ b/scripts/mintlify-post-processing/category-map.json @@ -0,0 +1,4 @@ +{ + "functions": "Main Methods", + "interfaces": "Modules" +} \ No newline at end of file diff --git a/scripts/mintlify-post-processing/file-processing/docs-json-template.json b/scripts/mintlify-post-processing/file-processing/docs-json-template.json new file mode 100644 index 0000000..b0dc864 --- /dev/null +++ b/scripts/mintlify-post-processing/file-processing/docs-json-template.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "mint", + "name": "Base44 Support Documentation", + "integrations": { + "mixpanel": { + "projectToken": "cc6e9e106e4b833fc3a3819c11b74138" + } + }, + "colors": { + "primary": "#FF5500", + "light": "#EEE2C0", + "dark": "#FF5500" + }, + "navigation": { + "tabs": [ + { + "tab": "SDK Reference", + "groups": [ + { + "group": "Main Methods", + "pages": [ + "content/functions/createClient", + "content/functions/createClientFromRequest", + "content/functions/getAccessToken", + "content/functions/saveAccessToken", + "content/functions/removeAccessToken", + "content/functions/getLoginUrl" + ] + }, + { + "group": "Modules", + "pages": ["content/interfaces/Auth"] + } + ] + } + ] + }, + "navbar": { + "links": [ + { + "label": "Support", + "href": "https://app.base44.com/support/conversations" + } + ], + "primary": { + "type": "button", + "label": "Base44", + "href": "https://base44.com/?utm_source=Mintlify&utm_medium=Main&utm_content=menu" + } + }, + "footer": { + "socials": { + "twitter": "https://x.com/base_44", + "discord": "https://discord.com/invite/ThpYPZpVts", + "linkedin": "https://www.linkedin.com/company/base44" + } + }, + "custom": { + "stylesheets": ["/styling.css"] + } +} diff --git a/scripts/mintlify-post-processing/file-processing/file-processing.js b/scripts/mintlify-post-processing/file-processing/file-processing.js new file mode 100755 index 0000000..ea2aa87 --- /dev/null +++ b/scripts/mintlify-post-processing/file-processing/file-processing.js @@ -0,0 +1,289 @@ +#!/usr/bin/env node + +/** + * Post-processing script for TypeDoc-generated MDX files + * + * TypeDoc now emits .mdx files directly, so this script: + * 1. Processes links to make them Mintlify-compatible + * 2. Removes files for linked types that should be suppressed + * 3. Cleans up the temporary linked types tracking file + * 4. Generates docs.json with navigation structure + * 5. Copies styling.css to docs directory + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const DOCS_DIR = path.join(__dirname, '..', '..', '..', 'docs'); +const CONTENT_DIR = path.join(DOCS_DIR, 'content'); +const LINKED_TYPES_FILE = path.join(CONTENT_DIR, '.linked-types.json'); +const TEMPLATE_PATH = path.join(__dirname, 'docs-json-template.json'); +const STYLING_CSS_PATH = path.join(__dirname, 'styling.css'); +const CATEGORY_MAP_PATH = path.join(__dirname, '../category-map.json'); +const TYPES_TO_EXPOSE_PATH = path.join(__dirname, '..', 'types-to-expose.json'); + +/** + * Get list of linked type names that should be suppressed + */ +function getLinkedTypeNames() { + try { + if (fs.existsSync(LINKED_TYPES_FILE)) { + const content = fs.readFileSync(LINKED_TYPES_FILE, 'utf-8'); + return new Set(JSON.parse(content)); + } + } catch (e) { + // If file doesn't exist or can't be read, return empty set + } + return new Set(); +} + +/** + * Load allow-listed type names that should remain in the docs output + */ +function getTypesToExpose() { + try { + const content = fs.readFileSync(TYPES_TO_EXPOSE_PATH, 'utf-8'); + const parsed = JSON.parse(content); + if (!Array.isArray(parsed)) { + throw new Error('types-to-expose.json must be an array of strings'); + } + return new Set(parsed); + } catch (e) { + console.error(`Error: Unable to read types-to-expose file: ${TYPES_TO_EXPOSE_PATH}`); + console.error(e.message); + process.exit(1); + } +} + +/** + * Process links in a file to make them Mintlify-compatible + */ +function processLinksInFile(filePath) { + let content = fs.readFileSync(filePath, 'utf-8'); + let modified = false; + + // Remove .md and .mdx extensions from markdown links + // This handles both relative and absolute paths + const linkRegex = /\[([^\]]+)\]\(([^)]+)(\.mdx?)\)/g; + const newContent = content.replace(linkRegex, (match, linkText, linkPath, ext) => { + modified = true; + return `[${linkText}](${linkPath})`; + }); + + if (modified) { + fs.writeFileSync(filePath, newContent, 'utf-8'); + return true; + } + + return false; +} + +/** + * Scan docs content directory and build navigation structure + */ +function scanDocsContent() { + const result = { + functions: [], + interfaces: [], + classes: [], + typeAliases: [], + }; + + const sections = ['functions', 'interfaces', 'classes', 'type-aliases']; + + for (const section of sections) { + const sectionDir = path.join(CONTENT_DIR, section); + if (!fs.existsSync(sectionDir)) continue; + + const files = fs.readdirSync(sectionDir); + const mdxFiles = files + .filter((file) => file.endsWith('.mdx')) + .map((file) => path.basename(file, '.mdx')) + .sort() + .map((fileName) => `content/${section}/${fileName}`); + + const key = section === 'type-aliases' ? 'typeAliases' : section; + result[key] = mdxFiles; + } + + return result; +} + + +/** + * Get group name for a section, using category map or default + */ +function getGroupName(section, categoryMap) { + if (categoryMap[section]) { + return categoryMap[section]; + } + + return section; +} + +/** + * Generate docs.json from template and scanned content + */ +function generateDocsJson(docsContent) { + const template = JSON.parse(fs.readFileSync(TEMPLATE_PATH, 'utf-8')); + let categoryMap = {}; + try { + categoryMap = JSON.parse(fs.readFileSync(CATEGORY_MAP_PATH, 'utf-8')); + } catch (e) { + // If file doesn't exist or can't be read, return empty object + console.error(`Error: Category map file not found: ${CATEGORY_MAP_PATH}`); + } + + const groups = []; + + if (docsContent.functions.length > 0 && categoryMap.functions) { + groups.push({ + group: getGroupName('functions', categoryMap), + pages: docsContent.functions, + }); + } + + if (docsContent.interfaces.length > 0 && categoryMap.interfaces) { + groups.push({ + group: getGroupName('interfaces', categoryMap), + pages: docsContent.interfaces, + }); + } + + if (docsContent.classes.length > 0 && categoryMap.classes) { + groups.push({ + group: getGroupName('classes', categoryMap), + pages: docsContent.classes, + }); + } + + if (docsContent.typeAliases.length > 0 && categoryMap['type-aliases']) { + groups.push({ + group: getGroupName('typeAliases', categoryMap), + pages: docsContent.typeAliases, + }); + } + + // Find or create SDK Reference tab + let sdkTab = template.navigation.tabs.find(tab => tab.tab === 'SDK Reference'); + if (!sdkTab) { + sdkTab = { tab: 'SDK Reference', groups: [] }; + template.navigation.tabs.push(sdkTab); + } + + sdkTab.groups = groups; + + const docsJsonPath = path.join(DOCS_DIR, 'docs.json'); + fs.writeFileSync(docsJsonPath, JSON.stringify(template, null, 2) + '\n', 'utf-8'); + console.log(`Generated docs.json`); +} + +/** + * Copy styling.css to docs directory + */ +function copyStylingCss() { + const targetPath = path.join(DOCS_DIR, 'styling.css'); + fs.copyFileSync(STYLING_CSS_PATH, targetPath); + console.log(`Copied styling.css`); +} + +/** + * Recursively process all MDX files + */ +function isTypeDocPath(relativePath) { + const normalized = relativePath.split(path.sep).join('/'); + return normalized.startsWith('content/interfaces/') || + normalized.startsWith('content/type-aliases/') || + normalized.startsWith('content/classes/'); +} + +/** + * Recursively process all MDX files + */ +function processAllFiles(dir, linkedTypeNames, exposedTypeNames) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + processAllFiles(entryPath, linkedTypeNames, exposedTypeNames); + } else if (entry.isFile() && (entry.name.endsWith('.mdx') || entry.name.endsWith('.md'))) { + // Extract the type name from the file path + // e.g., "docs/interfaces/LoginViaEmailPasswordResponse.mdx" -> "LoginViaEmailPasswordResponse" + const fileName = path.basename(entryPath, path.extname(entryPath)); + const relativePath = path.relative(DOCS_DIR, entryPath); + const isTypeDoc = isTypeDocPath(relativePath); + const isExposedType = !isTypeDoc || exposedTypeNames.has(fileName); + + // Remove any type doc files that are not explicitly exposed + if (isTypeDoc && !isExposedType) { + fs.unlinkSync(entryPath); + console.log(`Removed (not exposed): ${relativePath}`); + continue; + } + + // Remove suppressed linked type files (legacy behavior) as long as they aren't exposed + if (linkedTypeNames.has(fileName) && !exposedTypeNames.has(fileName)) { + fs.unlinkSync(entryPath); + console.log(`Removed (suppressed): ${relativePath}`); + } else { + // Process links in the file + if (processLinksInFile(entryPath)) { + console.log(`Processed links: ${relativePath}`); + } + } + } + } +} + +/** + * Main function + */ +function main() { + console.log('Processing TypeDoc MDX files for Mintlify...\n'); + + if (!fs.existsSync(DOCS_DIR)) { + console.error(`Error: Documentation directory not found: ${DOCS_DIR}`); + console.error('Please run "npm run docs:generate" first.'); + process.exit(1); + } + + // Get list of linked types to suppress + const linkedTypeNames = getLinkedTypeNames(); + const exposedTypeNames = getTypesToExpose(); + + // Process all files (remove suppressed ones and fix links) + // Process content directory specifically + if (fs.existsSync(CONTENT_DIR)) { + processAllFiles(CONTENT_DIR, linkedTypeNames, exposedTypeNames); + } else { + // Fallback to processing entire docs directory + processAllFiles(DOCS_DIR, linkedTypeNames, exposedTypeNames); + } + + // Clean up the linked types file + try { + if (fs.existsSync(LINKED_TYPES_FILE)) { + fs.unlinkSync(LINKED_TYPES_FILE); + } + } catch (e) { + // Ignore errors + } + + // Scan content and generate docs.json + const docsContent = scanDocsContent(); + generateDocsJson(docsContent); + + // Copy styling.css + copyStylingCss(); + + console.log(`\n✓ Post-processing complete!`); + console.log(` Documentation directory: ${DOCS_DIR}`); +} + +main(); diff --git a/scripts/mintlify-post-processing/file-processing/styling.css b/scripts/mintlify-post-processing/file-processing/styling.css new file mode 100644 index 0000000..430509e --- /dev/null +++ b/scripts/mintlify-post-processing/file-processing/styling.css @@ -0,0 +1,107 @@ +@import url('https://fonts.googleapis.com/css2?family=Wix+Madefor+Display:wght@400..800&family=Wix+Madefor+Text:ital,wght@0,400..800;1,400..800&display=swap'); + +/* Apply Wix Madefor Text for body text */ +body, +.mint-container { + font-family: "Wix Madefor Text", sans-serif; + font-weight: 400; + font-style: normal; + font-optical-sizing: auto; +} + +/* Apply Wix Madefor Display for headings */ +h1, h2, h3 { + font-family: "Wix Madefor Display", sans-serif; + font-weight: 600; + font-style: normal; + font-optical-sizing: auto; +} + +@media (prefers-color-scheme: light) { + body, + .mint-container, + h1, h2, h3 { + color: #111111; + } +} + + +/* Mintlify primary navbar CTA button color override */ +.mint-navbar .mint-button-primary, +.mint-navbar li#topbar-cta-button a > span.bg-primary-dark { + background-color: #FF5500 !important; /* updated brand orange */ + color: #111111 !important; + font-weight: 600; + border: none; +} + +/* If button has hover/focus/active states, ensure text stays black: */ +.mint-navbar .mint-navbarPrimaryButton:hover, +.mint-navbar .mint-navbarPrimaryButton:focus, +.mint-navbar .mint-navbarPrimaryButton:active, +.mint-navbar .mint-button-primary:hover, +.mint-navbar .mint-button-primary:focus, +.mint-navbar .mint-button-primary:active { + color: #111111 !important; +} + +/* Optional: Remove box-shadow on click, if present */ +.mint-navbar .mint-navbarPrimaryButton:active { + box-shadow: none !important; +} + +/* ---- NEW: Force the Base44 button text to black ---- */ +a[href*="base44.com"] span, +a[href*="base44.com"] .text-white, +.mint-navbar a span.text-white { + color: #111111 !important; +} +/* Force the ">" icon in the Base44 button to orange for consistency and accessibility */ +a[href*="base44.com"] svg, +a[href*="base44.com"] .text-white, +a[href*="base44.com"] svg path, +.mint-navbar a svg.text-white { + color: #111111 !important; /* Works if color is set by text utility */ + fill: #111111 !important; /* Ensures SVG/path fill is overridden */ +} +/* Restore thin chevron style for the navbar's "Base44" button arrow */ +a[href*="base44.com"] svg, +.mint-navbar a svg { + color: #111111 !important; + fill: none !important; + stroke: currentColor !important; + stroke-width: 2px !important; +} +a[href*="base44.com"] path, +.mint-navbar a svg path { + color: #111111 !important; + fill: none !important; + stroke: currentColor !important; + stroke-width: 2px !important; +} +/* Make the ">" icon (chevron) in Base44 navbar button thin and on-brand */ +a[href*="base44.com"] svg, +.mint-navbar a svg { + color: #111111 !important; +} + +a[href*="base44.com"] svg path, +.mint-navbar a svg path { + color: #111111 !important; + fill: none !important; + stroke: currentColor !important; + stroke-width: 1.5 !important; /* set to the original */ + stroke-linecap: round !important; /* keep the smooth end */ +} + +/* Optional: if you want it even thinner, try 1.2 or 1 */ + +/* Always use Mintlify's built-in theme variables! */ +div.prose.mt-1.font-normal.text-sm.leading-6.text-gray-600.dark\:text-gray-400, +div.prose.mt-1.font-normal.text-sm.leading-6.text-gray-600.dark\:text-gray-400 span, +.card .prose, +.card .prose span, +.mint-card .prose, +.mint-card .prose span { + color: var(--mint-text-secondary) !important; +} diff --git a/scripts/mintlify-post-processing/push-to-docs-repo.js b/scripts/mintlify-post-processing/push-to-docs-repo.js new file mode 100644 index 0000000..5876860 --- /dev/null +++ b/scripts/mintlify-post-processing/push-to-docs-repo.js @@ -0,0 +1,183 @@ +#!/usr/bin/env node + +import fs from "fs"; +import path from "path"; +import os from "os"; +import { execSync } from "child_process"; + +console.debug = () => {}; // Disable debug logging. Comment this out to enable debug logging. + +const DOCS_SOURCE_PATH = path.join(import.meta.dirname, "../../docs/content"); +const TARGET_DOCS_REPO_URL = "git@github.com:base44-dev/mintlify-docs.git"; +const CATEGORY_MAP_PATH = path.join(import.meta.dirname, "./category-map.json"); + +function parseArgs() { + const args = process.argv.slice(2); + let branch = null; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === "--branch" && i + 1 < args.length) { + branch = args[++i]; + } + } + return { branch }; +} + +function scanSdkDocs(sdkDocsDir) { + const result = {}; + + // Get a list of all the subdirectories in the sdkDocsDir + const subdirectories = fs.readdirSync(sdkDocsDir).filter(file => fs.statSync(path.join(sdkDocsDir, file)).isDirectory()); + console.log(`Subdirectories: ${subdirectories}`); + + for (const subdirectory of subdirectories) { + const subdirectoryPath = path.join(sdkDocsDir, subdirectory); + const files = fs.readdirSync(subdirectoryPath).filter(file => file.endsWith(".mdx")); + result[subdirectory] = files.map(file => path.basename(file, ".mdx")); + } + return result; +} + +function updateDocsJson(repoDir, sdkFiles) { + const docsJsonPath = path.join(repoDir, 'docs.json'); + let categoryMap = {}; + try { + categoryMap = JSON.parse(fs.readFileSync(CATEGORY_MAP_PATH, 'utf8')); + } catch (e) { + console.error(`Error: Category map file not found: ${CATEGORY_MAP_PATH}`); + process.exit(1); + } + + console.log(`Reading docs.json from ${docsJsonPath}...`); + const docsContent = fs.readFileSync(docsJsonPath, 'utf8'); + const docs = JSON.parse(docsContent); + + // Find the "SDK Reference" tab + let sdkTab = docs.navigation.tabs.find(tab => tab.tab === 'SDK Reference'); + + if (!sdkTab) { + console.log("Could not find 'SDK Reference' tab in docs.json. Creating it..."); + sdkTab = { + tab: 'SDK Reference', + groups: [] + }; + } + + // Update the groups + const newGroups = []; + + if(sdkFiles.functions.length > 0 && categoryMap.functions) { + newGroups.push({ + group: categoryMap.functions, + pages: sdkFiles.functions.map(file => `sdk-docs/functions/${file}`) + }); + } + + if(sdkFiles.interfaces.length > 0 && categoryMap.interfaces) { + newGroups.push({ + group: categoryMap.interfaces, + pages: sdkFiles.interfaces.map(file => `sdk-docs/interfaces/${file}`) + }); + } + + if(sdkFiles.classes.length > 0 && categoryMap.classes) { + newGroups.push({ + group: categoryMap.classes, + pages: sdkFiles.classes.map(file => `sdk-docs/classes/${file}`) + }); + } + + if(sdkFiles['type-aliases'].length > 0 && categoryMap['type-aliases']) { + newGroups.push({ + group: categoryMap['type-aliases'], + pages: sdkFiles['type-aliases'].map(file => `sdk-docs/type-aliases/${file}`) + }); + } + + sdkTab.groups = newGroups; + docs.navigation.tabs.push(sdkTab); + + console.debug(`New groups for docs.json: ${JSON.stringify(newGroups, null, 2)}`); + + // Write updated docs.json + console.log(`Writing updated docs.json to ${docsJsonPath}...`); + fs.writeFileSync(docsJsonPath, JSON.stringify(docs, null, 2) + '\n', 'utf8'); + + console.log('Successfully updated docs.json'); + } + +function main() { + const { branch } = parseArgs(); + if (!branch) { + console.error("Error: --branch is required"); + process.exit(1); + } + + console.log(`Branch: ${branch}`); + + if ( + !fs.existsSync(DOCS_SOURCE_PATH) || + !fs.statSync(DOCS_SOURCE_PATH).isDirectory() + ) { + console.error(`Error: docs directory does not exist: ${DOCS_SOURCE_PATH}`); + process.exit(1); + } + + let tempRepoDir; + try{ + // Create temporary directory + tempRepoDir = fs.mkdtempSync( + path.join(os.tmpdir(), "mintlify-docs-") + ); + // Clone the repository + console.log(`Cloning repository to ${tempRepoDir}...`); + execSync(`git clone ${TARGET_DOCS_REPO_URL} ${tempRepoDir}`); + + // Check if the specified branch already exists remotely + const branchExists = execSync(`git ls-remote --heads origin ${branch}`, { + cwd: tempRepoDir, + encoding: 'utf8' + }).trim().length > 0; + + if (branchExists) { + console.log(`Branch ${branch} already exists. Checking it out...`); + execSync(`git checkout -b ${branch} origin/${branch}`, { cwd: tempRepoDir }); + } else { + console.log(`Branch ${branch} does not exist. Creating it...`); + execSync(`git checkout -b ${branch}`, { cwd: tempRepoDir }); + } + + // Remove the existing sdk-docs directory + fs.rmSync(path.join(tempRepoDir, "sdk-docs"), { recursive: true, force: true }); + + // Copy the docs directory to the temporary repository + fs.cpSync(DOCS_SOURCE_PATH, path.join(tempRepoDir, "sdk-docs"), { recursive: true }); + + // Scan the sdk-docs directory + const sdkDocsDir = path.join(tempRepoDir, "sdk-docs"); + const sdkFiles = scanSdkDocs(sdkDocsDir); + + console.debug(`SDK files: ${JSON.stringify(sdkFiles, null, 2)}`); + + // Update the docs.json file + updateDocsJson(tempRepoDir, sdkFiles); + + // Commit the changes + execSync(`git add docs.json`, { cwd: tempRepoDir }); + execSync(`git add sdk-docs`, { cwd: tempRepoDir }); + execSync(`git commit -m "Auto-updates to SDK Reference Docs"`, { cwd: tempRepoDir }); + execSync(`git push --set-upstream origin ${branch}`, { cwd: tempRepoDir }); + + console.log("Successfully committed and pushed the changes"); + } catch (e) { + console.error(`Error: Failed to commit and push changes: ${e}`); + process.exit(1); + } finally { + // Remove the temporary directory + fs.rmSync(tempRepoDir, { recursive: true, force: true }); + } +} + +main(); \ No newline at end of file diff --git a/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js new file mode 100644 index 0000000..6bf1e0a --- /dev/null +++ b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-content.js @@ -0,0 +1,124 @@ +/** + * Content transformation functions (examples, frontmatter, etc.) + */ + +/** + * Add headings to CodeGroups that don't have them + */ +export function addHeadingsToCodeGroups(content) { + const lines = content.split('\n'); + const result = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + // Check if this line contains + if (line.trim() === '' || line.includes('')) { + // Look back up to 3 lines to see if there's a heading + let hasHeading = false; + for (let j = Math.max(0, i - 3); j < i; j++) { + const prevLine = lines[j].trim(); + if (/^#{2,4}\s+/.test(prevLine)) { + hasHeading = true; + break; + } + } + + // If no heading found, add one + if (!hasHeading) { + result.push('## Examples'); + result.push(''); + } + } + + result.push(line); + } + + return result.join('\n'); +} + +/** + * Convert code examples to Mintlify CodeGroup + */ +export function convertExamplesToCodeGroup(content) { + // Match Example/Examples headings from level 2-4 and capture their content until next section + const exampleSectionRegex = /^(#{2,4})\s+(Example|Examples)\s*$([\s\S]*?)(?=^#{2,4}\s|\n<\/ResponseField>|\n\*\*\*|$(?!\n))/gm; + + return content.replace(exampleSectionRegex, (match, headingLevel, exampleHeading, exampleContent) => { + const codeBlockRegex = /```([\w-]*)\s*([^\n]*)\n([\s\S]*?)```/g; + const examples = []; + let codeMatch; + + while ((codeMatch = codeBlockRegex.exec(exampleContent)) !== null) { + const language = codeMatch[1] || 'typescript'; + const titleFromCodeFence = codeMatch[2].trim(); + const code = codeMatch[3].trimEnd(); + + let title; + if (titleFromCodeFence && titleFromCodeFence.length > 0 && titleFromCodeFence.length < 100) { + // Strip comment markers from title (e.g., "// Basic example" -> "Basic example") + title = titleFromCodeFence.replace(/^\/\/\s*/, '').replace(/^\/\*\s*|\s*\*\/$/g, '').trim(); + } else { + title = examples.length === 0 ? 'Example' : `Example ${examples.length + 1}`; + } + + examples.push({ + title, + language, + code, + }); + } + + if (examples.length === 0) { + return match; + } + + // Use the original heading level, default to ## if not specified + const headingPrefix = headingLevel || '##'; + // Use "Examples" if multiple examples, otherwise "Example" + const headingText = examples.length > 1 ? 'Examples' : 'Example'; + + let codeGroup = `${headingPrefix} ${headingText}\n\n\n\n`; + + for (const example of examples) { + codeGroup += '```' + example.language + ' ' + example.title + '\n'; + codeGroup += example.code + '\n'; + codeGroup += '```\n\n'; + } + + codeGroup += '\n'; + + return codeGroup; + }); +} + +/** + * Add Mintlify frontmatter to the page + */ +export function addMintlifyFrontmatter(content, page) { + const titleMatch = content.match(/^#\s+(.+)$/m); + let title = titleMatch ? titleMatch[1].trim() : page?.model?.name || 'Documentation'; + + // Clean up title + title = title.replace(/\*\*/g, '').replace(/`/g, '').trim(); + title = title.replace(/^(?:Interface|Class|Type|Module|Function|Variable|Constant|Enum):\s*/i, '').trim(); + + const escapeYaml = (str) => { + if (str.includes(':') || str.includes('"') || str.includes("'") || str.includes('\n')) { + return str.replace(/"/g, '\\"'); + } + return str; + }; + + const frontmatter = `--- +title: "${escapeYaml(title)}" +--- + +`; + + // Remove the original h1 title (it's now in frontmatter) + const processedContent = content.replace(/^#\s+.+\n\n?/, ''); + + return frontmatter + processedContent; +} + diff --git a/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-linked-types.js b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-linked-types.js new file mode 100644 index 0000000..5f9b028 --- /dev/null +++ b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-linked-types.js @@ -0,0 +1,383 @@ +/** + * Linked type extraction and property parsing functions + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { ReflectionKind } from 'typedoc'; + +/** + * Extract properties from a linked type using TypeDoc's reflection API + */ +export function extractPropertiesFromLinkedType(linkedTypeInfo, context) { + if (!linkedTypeInfo || !context) { + return []; + } + + const { typeName } = linkedTypeInfo; + const { app, page } = context; + + try { + // First, try to get the type from TypeDoc's reflection API + if (app && page && page.model) { + const properties = extractPropertiesFromReflection(typeName, app, page); + if (properties.length > 0) { + return properties; + } + } + + // Fallback: try to read from generated markdown file + return extractPropertiesFromMarkdownFile(linkedTypeInfo, context); + } catch (error) { + console.warn(`Error extracting properties for type ${typeName}:`, error.message); + return []; + } +} + +/** + * Extract properties from TypeDoc's reflection API (preferred method) + */ +function extractPropertiesFromReflection(typeName, app, page) { + try { + // Access the project through the page's model + const project = page.model?.project; + if (!project) { + return []; + } + + // Find the type reflection in the project + const typeReflection = findReflectionByName(project, typeName); + if (!typeReflection) { + return []; + } + + // Extract properties from the reflection + const properties = []; + + // For interfaces and type aliases with properties + if (typeReflection.children) { + for (const child of typeReflection.children) { + if (child.kind === ReflectionKind.Property) { + const property = { + name: child.name, + type: getTypeString(child.type), + description: child.comment?.summary?.map(p => p.text).join('') || '', + optional: isOptional(child), + nested: [] + }; + + // Check for nested properties if the type is an object + if (child.type && isObjectLikeType(child.type)) { + property.nested = extractNestedProperties(child.type); + } + + properties.push(property); + } + } + } + + return properties; + } catch (error) { + console.warn(`Error extracting properties from reflection for ${typeName}:`, error.message); + return []; + } +} + +/** + * Find a reflection by name in the project + */ +function findReflectionByName(reflection, name) { + if (reflection.name === name) { + return reflection; + } + + if (reflection.children) { + for (const child of reflection.children) { + const found = findReflectionByName(child, name); + if (found) return found; + } + } + + return null; +} + +/** + * Get a string representation of a type + */ +function getTypeString(type) { + if (!type) return 'any'; + + switch (type.type) { + case 'intrinsic': + return type.name; + case 'reference': + return type.name; + case 'array': + return `${getTypeString(type.elementType)}[]`; + case 'union': + return type.types?.map(t => getTypeString(t)).join(' | ') || 'any'; + case 'intersection': + return type.types?.map(t => getTypeString(t)).join(' & ') || 'any'; + case 'literal': + return JSON.stringify(type.value); + case 'reflection': + return 'object'; + default: + return type.name || 'any'; + } +} + +/** + * Check if a property is optional + */ +function isOptional(child) { + return child.flags?.isOptional || false; +} + +/** + * Check if a type is object-like (has properties) + */ +function isObjectLikeType(type) { + return type.type === 'reflection' && type.declaration?.children; +} + +/** + * Extract nested properties from an object type + */ +function extractNestedProperties(type) { + if (!isObjectLikeType(type)) { + return []; + } + + const nested = []; + if (type.declaration?.children) { + for (const child of type.declaration.children) { + if (child.kind === ReflectionKind.Property) { + nested.push({ + name: child.name, + type: getTypeString(child.type), + description: child.comment?.summary?.map(p => p.text).join('') || '', + optional: isOptional(child) + }); + } + } + } + + return nested; +} + +/** + * Fallback: Extract properties from a linked type's markdown file + */ +function extractPropertiesFromMarkdownFile(linkedTypeInfo, context) { + const { typePath, typeName } = linkedTypeInfo; + const { currentPagePath, app } = context; + + if (!app || !app.options) { + return []; + } + + try { + // Get the output directory from TypeDoc (usually 'docs') + const outputDir = app.options.getValue('out') || 'docs'; + + // Convert relative link to file path + // Links can be: + // - Just the type name: "LoginViaEmailPasswordResponse" + // - Relative path: "../interfaces/LoginViaEmailPasswordResponse" or "./interfaces/LoginViaEmailPasswordResponse" + // - Absolute-looking: "interfaces/LoginViaEmailPasswordResponse" + let filePath; + + // Remove .md or .mdx extension if present + let cleanTypePath = typePath.replace(/\.(md|mdx)$/, ''); + + if (cleanTypePath.startsWith('../') || cleanTypePath.startsWith('./')) { + // Relative path - resolve from current page's directory + const currentDir = path.dirname(path.join(outputDir, currentPagePath || '')); + const basePath = path.resolve(currentDir, cleanTypePath); + + // Try .mdx first, then .md + if (!basePath.endsWith('.md') && !basePath.endsWith('.mdx')) { + const mdxPath = basePath + '.mdx'; + const mdPath = basePath + '.md'; + filePath = fs.existsSync(mdxPath) ? mdxPath : mdPath; + } else { + filePath = basePath; + } + } else if (cleanTypePath.includes('/')) { + // Path with directory separator + filePath = path.join(outputDir, cleanTypePath); + + // Try .mdx first, then .md + if (!filePath.endsWith('.md') && !filePath.endsWith('.mdx')) { + const mdxPath = filePath + '.mdx'; + const mdPath = filePath + '.md'; + filePath = fs.existsSync(mdxPath) ? mdxPath : mdPath; + } + } else { + // Just the type name - try interfaces/ first, then type-aliases/ + // Try .mdx first, then .md + filePath = path.join(outputDir, 'interfaces', cleanTypePath + '.mdx'); + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'interfaces', cleanTypePath + '.md'); + } + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'type-aliases', cleanTypePath + '.mdx'); + } + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'type-aliases', cleanTypePath + '.md'); + } + } + + // Normalize the path + filePath = path.normalize(filePath); + + // Check if file exists + if (!fs.existsSync(filePath)) { + // Don't warn during generation - the file might not exist yet + return []; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + return parsePropertiesFromTypeFile(content); + } catch (error) { + // Silent failure during generation + return []; + } +} + +/** + * Parse properties from a type file's markdown content + */ +function parsePropertiesFromTypeFile(content) { + const properties = []; + const lines = content.split('\n'); + + // Find the Properties section + let inPropertiesSection = false; + let i = 0; + + while (i < lines.length) { + const line = lines[i]; + + // Start of Properties section + if (line.match(/^##\s+Properties\s*$/)) { + inPropertiesSection = true; + i++; + continue; + } + + // Stop at next top-level heading (##) + if (inPropertiesSection && line.match(/^##\s+/) && !line.match(/^##\s+Properties\s*$/)) { + break; + } + + // Parse property: ### propertyName or ### propertyName? + if (inPropertiesSection && line.match(/^###\s+/)) { + const propMatch = line.match(/^###\s+(.+)$/); + if (propMatch) { + const rawName = propMatch[1].trim(); + const optional = rawName.endsWith('?'); + // Unescape markdown escapes (e.g., access\_token -> access_token) + let name = optional ? rawName.slice(0, -1).trim() : rawName.trim(); + name = name.replace(/\\_/g, '_').replace(/\\\*/g, '*').replace(/\\`/g, '`'); + + i++; + // Skip blank lines + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + // Get type from next line: > **name**: `type` or > `optional` **name**: `type` + let type = 'any'; + if (i < lines.length && lines[i].includes('`')) { + const typeMatch = lines[i].match(/`([^`]+)`/); + if (typeMatch) { + type = typeMatch[1].trim(); + } + i++; + } + + // Skip blank lines + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + // Collect description and nested properties + const descriptionLines = []; + const nested = []; + + // Look for nested properties (#### heading) + while (i < lines.length) { + const nextLine = lines[i]; + // Stop at next property (###) or section end (## or ***) + if (nextLine.match(/^###\s+/) || nextLine.match(/^##\s+/) || nextLine === '***') { + break; + } + + // Check for nested property (####) + if (nextLine.match(/^####\s+/)) { + const nestedMatch = nextLine.match(/^####\s+(.+)$/); + if (nestedMatch) { + const nestedRawName = nestedMatch[1].trim(); + const nestedOptional = nestedRawName.endsWith('?'); + // Unescape markdown escapes + let nestedName = nestedOptional ? nestedRawName.slice(0, -1).trim() : nestedRawName.trim(); + nestedName = nestedName.replace(/\\_/g, '_').replace(/\\\*/g, '*').replace(/\\`/g, '`'); + + i++; + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + let nestedType = 'any'; + if (i < lines.length && lines[i].includes('`')) { + const nestedTypeMatch = lines[i].match(/`([^`]+)`/); + if (nestedTypeMatch) { + nestedType = nestedTypeMatch[1].trim(); + } + i++; + } + + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + const nestedDescLines = []; + while (i < lines.length && !lines[i].match(/^####\s+/) && !lines[i].match(/^###\s+/) && + !lines[i].match(/^##\s+/) && lines[i] !== '***') { + nestedDescLines.push(lines[i]); + i++; + } + + nested.push({ + name: nestedName, + type: nestedType, + description: nestedDescLines.join('\n').trim(), + optional: nestedOptional + }); + continue; + } + } + + descriptionLines.push(nextLine); + i++; + } + + properties.push({ + name, + type, + description: descriptionLines.join('\n').trim(), + optional, + nested + }); + continue; + } + } + + i++; + } + + return properties; +} + diff --git a/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-parameters.js b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-parameters.js new file mode 100644 index 0000000..ea375d6 --- /dev/null +++ b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-parameters.js @@ -0,0 +1,550 @@ +/** + * Parameter conversion functions for TypeDoc Mintlify plugin + */ + +import { escapeAttribute } from './typedoc-mintlify-utils.js'; +import { extractPropertiesFromLinkedType } from './typedoc-mintlify-linked-types.js'; +import * as fs from 'fs'; +import * as path from 'path'; + +// Helper function to resolve type paths (similar to returns file) +function resolveTypePath(typeName, app, currentPagePath = null) { + const PRIMITIVE_TYPES = ['any', 'string', 'number', 'boolean', 'void', 'null', 'undefined', 'object', 'Array', 'Promise']; + + // Skip primitive types + if (PRIMITIVE_TYPES.includes(typeName)) { + return null; + } + + if (!app || !app.options) { + return null; + } + + const outputDir = app.options.getValue('out') || 'docs'; + + // Try interfaces/ first, then type-aliases/ + let filePath = path.join(outputDir, 'interfaces', typeName + '.mdx'); + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'interfaces', typeName + '.md'); + } + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'type-aliases', typeName + '.mdx'); + } + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'type-aliases', typeName + '.md'); + } + + if (fs.existsSync(filePath)) { + // Convert to relative path from current page if possible + if (currentPagePath) { + const currentDir = path.dirname(path.join(outputDir, currentPagePath)); + const relativePath = path.relative(currentDir, filePath).replace(/\\/g, '/'); + return relativePath.startsWith('.') ? relativePath : './' + relativePath; + } + // Otherwise return path relative to outputDir + return path.relative(outputDir, filePath).replace(/\\/g, '/'); + } + + return null; +} + +/** + * Convert top-level function parameters (## Parameters with ### param names) + */ +export function convertFunctionParameters(content, app = null, page = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + // Split content by ## headings to isolate the Parameters section + const sections = content.split(/\n(?=##\s+\w)/); + + return sections.map(section => { + // Only process ## Parameters sections (must start with exactly ##, not ###) + if (!section.match(/^##\s+Parameters\s*$/m)) { + return section; + } + + // Extract the content after "## Parameters" + const lines = section.split('\n'); + const paramStartIdx = lines.findIndex(l => l.match(/^##\s+Parameters\s*$/)); + + if (paramStartIdx === -1) return section; + + // Get everything after "## Parameters" line + const paramLines = lines.slice(paramStartIdx + 1); + const paramContent = paramLines.join('\n'); + + // Parse parameters with context for linked type resolution + const context = app && page ? { app, page, currentPagePath: page.url } : null; + const params = parseParametersWithExpansion(paramContent, '###', '####', context, linkedTypeNames, writeLinkedTypesFile); + + if (params.length === 0) return section; + + // Rebuild section with ParamFields + const beforeParams = lines.slice(0, paramStartIdx + 1).join('\n'); + return beforeParams + '\n\n' + buildParamFieldsSection(params, linkedTypeNames, writeLinkedTypesFile); + }).join('\n'); +} + +/** + * Convert interface method parameters (#### Parameters with ##### param names) + */ +export function convertInterfaceMethodParameters(content, app = null, page = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + const context = app && page ? { app, page, currentPagePath: page.url } : null; + return rewriteParameterSections(content, '#### Parameters', '#####', '######', context, linkedTypeNames, writeLinkedTypesFile); +} + +/** + * Convert class method parameters (#### Parameters with ##### param names) + */ +export function convertClassMethodParameters(content, app = null, page = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + const context = app && page ? { app, page, currentPagePath: page.url } : null; + return rewriteParameterSections(content, '#### Parameters', '#####', '######', context, linkedTypeNames, writeLinkedTypesFile); +} + +function rewriteParameterSections(content, sectionHeading, paramLevel, nestedLevel, context = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + const lines = content.split('\n'); + const result = []; + let i = 0; + + const isTerminatorLine = (line) => { + return ( + line.startsWith('#### Returns') || + line.startsWith('#### Example') || + line === '***' || + line.startsWith('### ') || + line.startsWith('## ') + ); + }; + + while (i < lines.length) { + const line = lines[i]; + if (line.startsWith(sectionHeading)) { + result.push(line); + i++; + const sectionStart = i; + while (i < lines.length && !isTerminatorLine(lines[i])) { + i++; + } + const sectionContentLines = lines.slice(sectionStart, i); + const sectionContent = sectionContentLines.join('\n').trim(); + // Use parseParametersWithExpansion if context is available, otherwise use parseParameters + const params = context + ? parseParametersWithExpansion(sectionContent, paramLevel, nestedLevel, context, linkedTypeNames, writeLinkedTypesFile) + : parseParameters(sectionContent, paramLevel, nestedLevel, context, linkedTypeNames, writeLinkedTypesFile); + if (params.length > 0) { + const block = buildParamFieldsSection(params, linkedTypeNames, writeLinkedTypesFile).trim(); + if (block) { + result.push(''); + result.push(...block.split('\n')); + result.push(''); + } + } else { + result.push(...sectionContentLines); + } + continue; + } + + result.push(line); + i++; + } + + return result.join('\n'); +} + +/** + * Parse parameters with type expansion (for functions) + */ +function parseParametersWithExpansion(paramContent, paramLevel, nestedLevel, context = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + const lines = paramContent.split('\n'); + const params = []; + + const isParamHeading = (line) => line.startsWith(paramLevel + ' '); + const isNestedHeading = nestedLevel ? (line) => line.startsWith(nestedLevel + ' ') : () => false; + const isTerminator = (line) => { + const trimmed = line.trim(); + if (!trimmed) return false; + if (trimmed.startsWith('#### Returns') || trimmed.startsWith('#### Example') || trimmed === '***') { + return true; + } + const nestedPrefix = nestedLevel ? nestedLevel + ' ' : null; + if (/^#{1,3}\s+/.test(trimmed)) { + if (!trimmed.startsWith(paramLevel + ' ') && !(nestedPrefix && trimmed.startsWith(nestedPrefix))) { + return true; + } + } + return false; + }; + + const extractType = (line) => { + if (!line) return null; + const trimmed = line.trim(); + + // Handle [`TypeName`](link) format first (backticks inside the link) + const linkWithBackticksMatch = trimmed.match(/^\[`([^`]+)`\]\(([^)]+)\)$/); + if (linkWithBackticksMatch) { + return { type: linkWithBackticksMatch[1], link: linkWithBackticksMatch[2] }; + } + + // Handle simple `TypeName` format + if (!trimmed.startsWith('`')) return null; + const simpleMatch = trimmed.match(/^`([^`]+)`$/); + if (simpleMatch) { + return { type: simpleMatch[1], link: null }; + } + return null; + }; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + if (!isParamHeading(line)) { + i++; + continue; + } + + let rawName = line.slice(paramLevel.length).trim(); + const optional = rawName.endsWith('?'); + const cleanName = optional ? rawName.slice(0, -1).trim() : rawName.trim(); + i++; + + // Skip blank lines + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + let type = 'any'; + let typeLink = null; + if (i < lines.length) { + const maybeType = extractType(lines[i]); + if (maybeType) { + if (typeof maybeType === 'object') { + type = maybeType.type; + typeLink = maybeType.link; + } else { + type = maybeType; + } + i++; + } + } + + // Skip blank lines after type + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + const descriptionLines = []; + while (i < lines.length && !isParamHeading(lines[i]) && !isNestedHeading(lines[i]) && !isTerminator(lines[i])) { + descriptionLines.push(lines[i]); + i++; + } + + // Check if we should expand this type inline + let linkedTypeInfo = typeLink ? { typeName: type, typePath: typeLink } : null; + let nested = []; + + // If we don't have a link but have a non-primitive type, try to resolve it + if (!linkedTypeInfo && type && type !== 'any' && context && context.app) { + const simpleTypeName = type.replace(/[<>\[\]]/g, '').trim(); + const PRIMITIVE_TYPES = ['any', 'string', 'number', 'boolean', 'void', 'null', 'undefined', 'object', 'Array', 'Promise']; + if (simpleTypeName && !PRIMITIVE_TYPES.includes(simpleTypeName)) { + // Try to resolve the type path + const typePath = resolveTypePath(simpleTypeName, context.app, context.currentPagePath); + // Track the type even if we can't resolve the path - it might be a linked type + linkedTypeInfo = { typeName: simpleTypeName, typePath: typePath || simpleTypeName }; + + // Track linked types for suppression + } + } + + // Track linked types for suppression (for types with explicit links) + + // Try to extract properties from the linked type + if (linkedTypeInfo && context) { + const properties = extractPropertiesFromLinkedType(linkedTypeInfo, context); + if (properties.length > 0) { + // Convert properties to nested format + nested = properties.map(prop => ({ + name: prop.name, + type: prop.type, + description: prop.description, + optional: prop.optional + })); + } + } + + // If no linked properties were found, check for manually specified nested fields + if (nested.length === 0) { + while (i < lines.length && isNestedHeading(lines[i])) { + let nestedRawName = lines[i].slice(nestedLevel.length).trim(); + const nestedOptional = nestedRawName.endsWith('?'); + const nestedName = nestedOptional ? nestedRawName.slice(0, -1).trim() : nestedRawName.trim(); + i++; + + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + let nestedType = 'any'; + if (i < lines.length) { + const maybeNestedType = extractType(lines[i]); + if (maybeNestedType) { + if (typeof maybeNestedType === 'object') { + nestedType = maybeNestedType.type; + } else { + nestedType = maybeNestedType; + } + i++; + } + } + + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + const nestedDescLines = []; + while (i < lines.length && !isNestedHeading(lines[i]) && !isParamHeading(lines[i]) && !isTerminator(lines[i])) { + nestedDescLines.push(lines[i]); + i++; + } + + nested.push({ + name: nestedName, + type: nestedType, + description: nestedDescLines.join('\n').trim(), + optional: nestedOptional + }); + } + } + + params.push({ + name: cleanName, + type: type, + description: descriptionLines.join('\n').trim(), + optional, + nested + }); + } + + return params; +} + +/** + * Parse parameters from markdown content (for interface/class methods - no expansion) + */ +function parseParameters(paramContent, paramLevel, nestedLevel, context = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + const lines = paramContent.split('\n'); + const params = []; + + const isParamHeading = (line) => line.startsWith(paramLevel + ' '); + const isNestedHeading = nestedLevel ? (line) => line.startsWith(nestedLevel + ' ') : () => false; + const isTerminator = (line) => { + const trimmed = line.trim(); + if (!trimmed) return false; + if (trimmed.startsWith('#### Returns') || trimmed.startsWith('#### Example') || trimmed === '***') { + return true; + } + const nestedPrefix = nestedLevel ? nestedLevel + ' ' : null; + if (/^#{1,3}\s+/.test(trimmed)) { + if (!trimmed.startsWith(paramLevel + ' ') && !(nestedPrefix && trimmed.startsWith(nestedPrefix))) { + return true; + } + } + return false; + }; + + const extractType = (line) => { + if (!line) return null; + const trimmed = line.trim(); + + // Handle [`TypeName`](link) format first (backticks inside the link) + const linkWithBackticksMatch = trimmed.match(/^\[`([^`]+)`\]\(([^)]+)\)$/); + if (linkWithBackticksMatch) { + return { type: linkWithBackticksMatch[1], link: linkWithBackticksMatch[2] }; + } + + // Handle simple `TypeName` format + if (!trimmed.startsWith('`')) return null; + const simpleMatch = trimmed.match(/^`([^`]+)`$/); + if (simpleMatch) { + return { type: simpleMatch[1], link: null }; + } + return null; + }; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + if (!isParamHeading(line)) { + i++; + continue; + } + + let rawName = line.slice(paramLevel.length).trim(); + const optional = rawName.endsWith('?'); + const cleanName = optional ? rawName.slice(0, -1).trim() : rawName.trim(); + i++; + + // Skip blank lines + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + let type = 'any'; + let typeLink = null; + if (i < lines.length) { + const maybeType = extractType(lines[i]); + if (maybeType) { + if (typeof maybeType === 'object') { + type = maybeType.type; + typeLink = maybeType.link; + } else { + type = maybeType; + } + i++; + } + } + + // Skip blank lines after type + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + const descriptionLines = []; + while (i < lines.length && !isParamHeading(lines[i]) && !isNestedHeading(lines[i]) && !isTerminator(lines[i])) { + descriptionLines.push(lines[i]); + i++; + } + + // Check if we should expand this type inline + const linkedTypeInfo = typeLink ? { typeName: type, typePath: typeLink } : null; + let nested = []; + + // Try to extract properties from the linked type + if (linkedTypeInfo && context) { + const properties = extractPropertiesFromLinkedType(linkedTypeInfo, context); + if (properties.length > 0) { + // Convert properties to nested format + nested = properties.map(prop => ({ + name: prop.name, + type: prop.type, + description: prop.description, + optional: prop.optional + })); + // Keep the type as the original type name (without expanding to 'object') + // This preserves the type name in the ParamField + } + } + + // If no linked properties were found, check for manually specified nested fields + if (nested.length === 0) { + while (i < lines.length && isNestedHeading(lines[i])) { + let nestedRawName = lines[i].slice(nestedLevel.length).trim(); + const nestedOptional = nestedRawName.endsWith('?'); + const nestedName = nestedOptional ? nestedRawName.slice(0, -1).trim() : nestedRawName.trim(); + i++; + + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + let nestedType = 'any'; + if (i < lines.length) { + const maybeNestedType = extractType(lines[i]); + if (maybeNestedType) { + if (typeof maybeNestedType === 'object') { + nestedType = maybeNestedType.type; + } else { + nestedType = maybeNestedType; + } + i++; + } + } + + while (i < lines.length && lines[i].trim() === '') { + i++; + } + + const nestedDescLines = []; + while (i < lines.length && !isNestedHeading(lines[i]) && !isParamHeading(lines[i]) && !isTerminator(lines[i])) { + nestedDescLines.push(lines[i]); + i++; + } + + nested.push({ + name: nestedName, + type: nestedType, + description: nestedDescLines.join('\n').trim(), + optional: nestedOptional + }); + } + } + + params.push({ + name: cleanName, + type: nested.length > 0 ? type : type, // Keep original type name + description: descriptionLines.join('\n').trim(), + optional, + nested + }); + } + + return params; +} + +/** + * Build ParamField components from parsed parameters + */ +function buildParamFieldsSection(params, linkedTypeNames = null, writeLinkedTypesFile = null) { + if (!params || params.length === 0) { + return ''; + } + + const PRIMITIVE_TYPES = ['any', 'string', 'number', 'boolean', 'void', 'null', 'undefined', 'object', 'Array', 'Promise']; + + let fieldsOutput = ''; + + for (const param of params) { + const requiredAttr = param.optional ? '' : ' required'; + + // Track non-primitive parameter types for suppression + + fieldsOutput += `\n`; + + // Always show description in ParamField if it exists + if (param.description) { + fieldsOutput += `\n${param.description}\n`; + } + + fieldsOutput += '\n\n'; + + // If param has nested fields, wrap them in an Accordion + if (param.nested.length > 0) { + // Accordion title is always "Properties" + fieldsOutput += `\n\n\n`; + + for (const nested of param.nested) { + const requiredAttr = nested.optional ? '' : ' required'; + fieldsOutput += `\n`; + + if (nested.description) { + fieldsOutput += `\n${nested.description}\n`; + } + + fieldsOutput += '\n\n\n'; + } + + fieldsOutput += '\n\n'; + } else { + fieldsOutput += '\n'; + } + } + + // Wrap multiple parameters in an Accordion (but not single parameters, even if they have nested fields) + const hasMultipleParams = params.length > 1; + + if (hasMultipleParams) { + return `\n\n${fieldsOutput.trim()}\n`; + } + + return fieldsOutput; +} + + diff --git a/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js new file mode 100644 index 0000000..85a613f --- /dev/null +++ b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js @@ -0,0 +1,189 @@ +/** + * TypeDoc plugin for Mintlify MDX output + * Hooks into TypeDoc's markdown renderer to customize output for Mintlify + */ + +import { MarkdownPageEvent } from 'typedoc-plugin-markdown'; +import { ReflectionKind, RendererEvent } from 'typedoc'; +import * as fs from 'fs'; +import * as path from 'path'; +import { convertFunctionParameters, convertInterfaceMethodParameters, convertClassMethodParameters } from './typedoc-mintlify-parameters.js'; +import { convertFunctionReturns, convertInterfaceMethodReturns, convertClassMethodReturns } from './typedoc-mintlify-returns.js'; +import { convertExamplesToCodeGroup, addMintlifyFrontmatter, addHeadingsToCodeGroups } from './typedoc-mintlify-content.js'; + +/** + * Plugin load function called by TypeDoc + */ +// Track interfaces that are linked types and should be suppressed +const linkedTypeNames = new Set(); + +/** + * Load function called by TypeDoc + */ +export function load(app) { + console.log('Loading Mintlify TypeDoc plugin...'); + + app.renderer.on(MarkdownPageEvent.END, (page) => { + if (!page.contents) return; + + let content = page.contents; + + // Determine what kind of page this is. + const isFunction = page.model?.kind === ReflectionKind.Function; + const isClass = page.model?.kind === ReflectionKind.Class; + const isInterface = page.model?.kind === ReflectionKind.Interface; + + + // 1. Remove breadcrumbs navigation + content = content.replace(/^\[.*?\]\(.*?\)\s*\n+/m, ''); + + // 2. Convert parameters to ParamField components and returns to ResponseField components + // Functions: ## Parameters/Returns with ### field names + // Interface methods: #### Parameters/Returns with ##### field names + // Class methods: #### Parameters/Returns with ##### field names + const writeLinkedTypesFile = createWriteLinkedTypesFile(app); + + if (isFunction) { + content = convertFunctionParameters(content, app, page, linkedTypeNames, writeLinkedTypesFile); + content = convertFunctionReturns(content, app, page, linkedTypeNames, writeLinkedTypesFile); + } else if (isInterface) { + content = convertInterfaceMethodParameters(content, app, page); + content = convertInterfaceMethodReturns(content, app, page, linkedTypeNames, writeLinkedTypesFile); + } else if (isClass) { + content = convertClassMethodParameters(content, app, page); + content = convertClassMethodReturns(content, app, page, linkedTypeNames, writeLinkedTypesFile); + } + + // 3. Convert code examples to CodeGroup + content = convertExamplesToCodeGroup(content); + + // 3a. Add headings to CodeGroups that don't have them + content = addHeadingsToCodeGroups(content); + + // 4. Remove links from function signatures (convert [`TypeName`](link) to `TypeName`) + content = content.replace(/\[`([^`]+)`\]\([^)]+\)/g, '`$1`'); + + // 5. Remove .md and .mdx extensions from links + content = content.replace(/\[([^\]]+)\]\(([^)]+)\.mdx?\)/g, '[$1]($2)'); + + // 6. Add on-page navigation panel + content = addOnThisPagePanel(content, page); + + // 7. Add frontmatter + content = addMintlifyFrontmatter(content, page); + + page.contents = content; + }); + + // Write linked types file once at the end of all processing + app.renderer.on(MarkdownPageEvent.END, () => { + // This will run after all pages are processed + // We'll write the file in a different event + }); + + // Write linked types file after all pages are processed + app.renderer.on(RendererEvent.END, () => { + const writeLinkedTypesFile = createWriteLinkedTypesFile(app); + writeLinkedTypesFile(); + }); +} + +/** + * Write linked types file + */ +function createWriteLinkedTypesFile(app) { + return () => { + if (!app || !app.options) { + return; + } + const outputDir = app.options.getValue('out') || 'docs'; + const resolvedOutputDir = path.resolve(outputDir); + const linkedTypesFile = path.join(resolvedOutputDir, '.linked-types.json'); + try { + // Ensure content directory exists + if (!fs.existsSync(resolvedOutputDir)) { + fs.mkdirSync(resolvedOutputDir, { recursive: true }); + } + fs.writeFileSync(linkedTypesFile, JSON.stringify(Array.from(linkedTypeNames)), 'utf-8'); + } catch (e) { + // Ignore errors writing the file + console.warn('Warning: Could not write linked types file:', e.message); + } + }; +} + +/** + * Insert a Mintlify Panel with links to method headings (###) for type docs + */ +function addOnThisPagePanel(content, page) { + const links = getPanelLinksForPage(page, content); + if (!links || links.length === 0) { + return content; + } + if (content.includes(' **On this page**')) { + return content; + } + + const panelLines = [ + '', + '', + ' **On this page**', + '', + ...links.map(({ heading, anchor }) => `- [${heading}](#${anchor})`), + '', + '', + '' + ]; + + const panelBlock = panelLines.join('\n'); + const firstSectionIndex = content.indexOf('\n## '); + if (firstSectionIndex === -1) { + return `${content}\n\n${panelBlock}`; + } + const insertionPoint = firstSectionIndex + 1; + return `${content.slice(0, insertionPoint)}${panelBlock}${content.slice(insertionPoint)}`; +} + +function getPanelLinksForPage(page, content) { + if (!page?.model || !page.url) { + return null; + } + if (isTypeDoc(page)) { + return extractHeadings(content, /^###\s+(.+?)\s*$/gm); + } + if (isFunctionDoc(page)) { + return extractHeadings(content, /^##\s+(.+?)\s*$/gm); + } + return null; +} + +function isTypeDoc(page) { + const allowedKinds = new Set([ReflectionKind.Interface, ReflectionKind.Class]); + return allowedKinds.has(page.model.kind) && + (page.url.startsWith('interfaces/') || page.url.startsWith('classes/')); +} + +function isFunctionDoc(page) { + return page.model.kind === ReflectionKind.Function && page.url.startsWith('functions/'); +} + +function extractHeadings(content, regex) { + const headings = []; + let match; + while ((match = regex.exec(content)) !== null) { + const heading = match[1].trim(); + if (!heading) { + continue; + } + headings.push({ heading, anchor: slugifyHeading(heading) }); + } + return headings; +} + +function slugifyHeading(text) { + return text + .toLowerCase() + .replace(/[`~!@#$%^&*()+={}\[\]|\\:;"'<>,.?]/g, '') + .replace(/\s+/g, '-'); +} + diff --git a/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js new file mode 100644 index 0000000..6dd0dea --- /dev/null +++ b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-returns.js @@ -0,0 +1,819 @@ +/** + * Return/Response field conversion functions for TypeDoc Mintlify plugin + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { extractPropertiesFromLinkedType } from './typedoc-mintlify-linked-types.js'; +import { escapeAttribute } from './typedoc-mintlify-utils.js'; + +const PRIMITIVE_TYPES = [ + 'any', + 'string', + 'number', + 'boolean', + 'void', + 'null', + 'undefined', + 'object', + 'Array', + 'Promise', +]; + +/** + * Extract signature information from content lines + */ +/** + * Try to resolve a type name to a documentation file path + */ +function resolveTypePath(typeName, app, currentPagePath = null) { + // Skip primitive types + if (PRIMITIVE_TYPES.includes(typeName)) { + return null; + } + + if (!app || !app.options) { + return null; + } + + const outputDir = app.options.getValue('out') || 'docs'; + + // Try interfaces/ first, then type-aliases/ + let filePath = path.join(outputDir, 'interfaces', typeName + '.mdx'); + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'interfaces', typeName + '.md'); + } + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'type-aliases', typeName + '.mdx'); + } + if (!fs.existsSync(filePath)) { + filePath = path.join(outputDir, 'type-aliases', typeName + '.md'); + } + + if (fs.existsSync(filePath)) { + // Convert to relative path from current page if possible + if (currentPagePath) { + const currentDir = path.dirname(path.join(outputDir, currentPagePath)); + const relativePath = path.relative(currentDir, filePath).replace(/\\/g, '/'); + return relativePath.startsWith('.') ? relativePath : './' + relativePath; + } + // Otherwise return path relative to outputDir + return path.relative(outputDir, filePath).replace(/\\/g, '/'); + } + + return null; +} + +export function extractSignatureInfo(lines, linkedTypeNames, writeLinkedTypesFile, app, currentPagePath = null) { + const signatureMap = new Map(); + const linkedTypeMap = new Map(); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + // Match function signature: > **methodName**(...): `returnType` or `returnType`\<`generic`\> + // Handle both simple types and generic types like `Promise`\<`any`\> or `Promise`\<[`TypeName`](link)\> + const sigMatch = line.match(/^>\s*\*\*(\w+)\*\*\([^)]*\):\s*`([^`]+)`(?:\\<(.+?)\\>)?/); + if (sigMatch) { + const methodName = sigMatch[1]; + let returnType = sigMatch[2]; + const genericParam = sigMatch[3]; + + // Check if generic parameter is a markdown link: [`TypeName`](link) + if (genericParam) { + const linkMatch = genericParam.match(/\[`([^`]+)`\]\(([^)]+)\)/); + if (linkMatch) { + const linkedTypeName = linkMatch[1]; + const linkedTypePath = linkMatch[2]; + returnType = `${returnType}<${linkedTypeName}>`; + linkedTypeMap.set(i, { typeName: linkedTypeName, typePath: linkedTypePath }); + // Track this type name so we can suppress its documentation page + if (linkedTypeNames) { + linkedTypeNames.add(linkedTypeName); + if (writeLinkedTypesFile) writeLinkedTypesFile(); + } + } else { + // Simple generic type without link - try to resolve it + const simpleGeneric = genericParam.replace(/`/g, '').trim(); + returnType = `${returnType}<${simpleGeneric}>`; + + // Try to resolve the type to a documentation file + const typePath = resolveTypePath(simpleGeneric, app, currentPagePath); + if (typePath) { + linkedTypeMap.set(i, { typeName: simpleGeneric, typePath: typePath }); + if (linkedTypeNames) { + linkedTypeNames.add(simpleGeneric); + if (writeLinkedTypesFile) writeLinkedTypesFile(); + } + } + } + } + // Store the return type with the signature line index as the key + signatureMap.set(i, returnType); + + // If we don't already have linked type info (e.g., non-generic return), + // try to resolve the return type to a documentation file + if (!linkedTypeMap.has(i)) { + const simpleTypeName = getSimpleTypeName(returnType); + if (simpleTypeName && !PRIMITIVE_TYPES.includes(simpleTypeName)) { + let typePath = resolveTypePath(simpleTypeName, app, currentPagePath); + if (!typePath) { + // Fallback to the raw type name so downstream parsing can still attempt resolution + typePath = simpleTypeName; + } + linkedTypeMap.set(i, { typeName: simpleTypeName, typePath }); + if (linkedTypeNames) { + linkedTypeNames.add(simpleTypeName); + if (writeLinkedTypesFile) writeLinkedTypesFile(); + } + } + } + } + } + + return { signatureMap, linkedTypeMap }; +} + +/** + * Convert function returns + */ +export function convertFunctionReturns(content, app, page, linkedTypeNames = null, writeLinkedTypesFile = null) { + // For functions, we need to extract signature info with linked types + const lines = content.split('\n'); + // Use provided linkedTypeNames Set or create a local one + const localLinkedTypeNames = linkedTypeNames || new Set(); + const localWriteLinkedTypesFile = writeLinkedTypesFile || (() => {}); + const { signatureMap, linkedTypeMap } = extractSignatureInfo(lines, localLinkedTypeNames, localWriteLinkedTypesFile, app, page?.url); + + return rewriteReturnSections(content, { + heading: '## Returns', + fieldHeading: '###', + nestedHeading: '####', + stopOnLevel3: false, + signatureMap, + linkedTypeMap, + app, + page, + linkedTypeNames: localLinkedTypeNames, + writeLinkedTypesFile: localWriteLinkedTypesFile, + }); +} + +/** + * Convert interface method returns + */ +export function convertInterfaceMethodReturns(content, app, page, linkedTypeNames, writeLinkedTypesFile) { + const lines = content.split('\n'); + const { signatureMap, linkedTypeMap } = extractSignatureInfo(lines, linkedTypeNames, writeLinkedTypesFile, app, page?.url); + + return rewriteReturnSections(content, { + heading: '#### Returns', + fieldHeading: '#####', + nestedHeading: '######', + stopOnLevel3: true, + signatureMap, + linkedTypeMap, + app, + page, + }); +} + +/** + * Convert class method returns + */ +export function convertClassMethodReturns(content, app, page, linkedTypeNames, writeLinkedTypesFile) { + const lines = content.split('\n'); + const { signatureMap, linkedTypeMap } = extractSignatureInfo(lines, linkedTypeNames, writeLinkedTypesFile, app, page?.url); + + return rewriteReturnSections(content, { + heading: '#### Returns', + fieldHeading: '#####', + nestedHeading: '######', + stopOnLevel3: true, + signatureMap, + linkedTypeMap, + app, + page, + }); +} + +function rewriteReturnSections(content, options) { + const { + heading, + fieldHeading, + nestedHeading, + stopOnLevel3, + signatureMap = new Map(), + linkedTypeMap = new Map(), + app, + page, + linkedTypeNames = null, + writeLinkedTypesFile = null + } = options; + const lines = content.split('\n'); + const result = []; + let i = 0; + + const isTerminatorLine = (line) => { + const trimmed = line.trim(); + if (!trimmed) return false; + if (trimmed.match(/^#{2,4}\s+Examples?/i) || trimmed === '***') { + return true; + } + if (heading !== '## Returns' && trimmed.startsWith('## ')) { + return true; + } + // For function Returns, stop at nested method definitions (#### methodName()) + if (heading === '## Returns' && trimmed.match(/^####\s+\w+\.?\w*\(\)/)) { + return true; + } + if (stopOnLevel3 && trimmed.startsWith('### ')) { + return true; + } + return false; + }; + + while (i < lines.length) { + const line = lines[i]; + if (line.startsWith(heading)) { + result.push(line); + i++; + const sectionStart = i; + while (i < lines.length && !isTerminatorLine(lines[i])) { + i++; + } + const sectionLines = lines.slice(sectionStart, i); + const sectionContent = sectionLines.join('\n').trim(); + + // For function Returns sections, parse nested fields (### headings) + if (heading === '## Returns') { + // Look backwards to find the function signature + let sigLineIdx = i - 2; // Go back past the Returns heading + while (sigLineIdx >= 0 && !lines[sigLineIdx].match(/^>\s*\*\*\w+\*\*\(/)) { + sigLineIdx--; + } + + // If we didn't find it by pattern, try to find it in our signature map + if (sigLineIdx < 0 || !signatureMap.has(sigLineIdx)) { + // Try searching a bit further back (up to 10 lines) + for (let j = i - 2; j >= Math.max(0, i - 12); j--) { + if (signatureMap.has(j)) { + sigLineIdx = j; + break; + } + } + } + + const returnTypeFromSignature = sigLineIdx >= 0 ? signatureMap.get(sigLineIdx) : null; + const linkedTypeInfo = sigLineIdx >= 0 ? linkedTypeMap.get(sigLineIdx) : null; + const context = app && page ? { app, page, currentPagePath: page.url } : null; + + // Get the type name for display - prefer linkedTypeInfo.typeName, fallback to returnTypeFromSignature + const returnTypeName = linkedTypeInfo?.typeName || returnTypeFromSignature; + + // Track linked type if found + if (linkedTypeInfo && linkedTypeNames) { + linkedTypeNames.add(linkedTypeInfo.typeName); + if (writeLinkedTypesFile) { + writeLinkedTypesFile(); + } + } + + const { fields, leadingText, extractedTypeName } = parseReturnFields( + sectionContent, + fieldHeading, + nestedHeading, + returnTypeFromSignature, + linkedTypeInfo, + context, + linkedTypeNames, + writeLinkedTypesFile + ); + if (fields.length === 0) { + result.push(...sectionLines); + } else { + if (leadingText) { + result.push(''); + result.push(leadingText); + } + // Use extractedTypeName if available, otherwise fallback to returnTypeName + const typeNameForDisplay = extractedTypeName || returnTypeName; + const fieldsBlock = formatReturnFieldsOutput(fields, typeNameForDisplay, linkedTypeNames, writeLinkedTypesFile); + if (fieldsBlock) { + result.push(''); + result.push(fieldsBlock); + result.push(''); + } + } + continue; + } + + // For interface/class method Returns sections + // The Returns section starts at i-1 (after the heading line) + // Look backwards to find the function signature + let sigLineIdx = i - 2; // Go back past the Returns heading + while (sigLineIdx >= 0 && !lines[sigLineIdx].match(/^>\s*\*\*\w+\*\*\(/)) { + sigLineIdx--; + } + + // If we didn't find it by pattern, try to find it in our signature map + // by checking a few lines before the Returns section + if (sigLineIdx < 0 || !signatureMap.has(sigLineIdx)) { + // Try searching a bit further back (up to 10 lines) + for (let j = i - 2; j >= Math.max(0, i - 12); j--) { + if (signatureMap.has(j)) { + sigLineIdx = j; + break; + } + } + } + + const returnTypeFromSignature = sigLineIdx >= 0 ? signatureMap.get(sigLineIdx) : null; + const linkedTypeInfo = sigLineIdx >= 0 ? linkedTypeMap.get(sigLineIdx) : null; + + // Get the type name for display - prefer linkedTypeInfo.typeName, fallback to returnTypeFromSignature + const returnTypeName = linkedTypeInfo?.typeName || returnTypeFromSignature; + + // Track linked type if found + if (linkedTypeInfo && linkedTypeNames) { + linkedTypeNames.add(linkedTypeInfo.typeName); + if (writeLinkedTypesFile) { + writeLinkedTypesFile(); + } + } + + const { fields, leadingText, extractedTypeName } = parseReturnFields( + sectionContent, + fieldHeading, + nestedHeading, + returnTypeFromSignature, + linkedTypeInfo, + { app, page, currentPagePath: page.url }, + linkedTypeNames, + writeLinkedTypesFile + ); + if (fields.length === 0) { + result.push(...sectionLines); + } else { + if (leadingText) { + result.push(''); + result.push(leadingText); + } + // Use extractedTypeName if available, otherwise fallback to returnTypeName + const typeNameForDisplay = extractedTypeName || returnTypeName; + const fieldsBlock = formatReturnFieldsOutput(fields, typeNameForDisplay, linkedTypeNames, writeLinkedTypesFile); + if (fieldsBlock) { + result.push(''); + result.push(fieldsBlock); + result.push(''); + } + } + continue; + } + + result.push(line); + i++; + } + + return result.join('\n'); +} + +function parseReturnFields(sectionContent, fieldHeading, nestedHeading, returnTypeFromSignature = null, linkedTypeInfo = null, context = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + if (!sectionContent) { + // If we have a linked type but no section content, try to extract from the linked type + if (linkedTypeInfo && context) { + const properties = extractPropertiesFromLinkedType(linkedTypeInfo, context); + if (properties.length > 0) { + // Return separate ResponseFields for each property (skip the default "result" field) + const resultFields = []; + + // Add a separate ResponseField for each property + for (const prop of properties) { + resultFields.push({ + name: prop.name, + type: prop.type, + description: prop.description, + optional: prop.optional, + nested: prop.nested || [] + }); + } + + return { + fields: resultFields, + leadingText: '', + extractedTypeName: linkedTypeInfo.typeName + }; + } + } + return { fields: [], leadingText: '', extractedTypeName: null }; + } + + const lines = sectionContent.split('\n'); + const fields = []; + const headingPrefix = fieldHeading ? `${fieldHeading} ` : null; + const nestedPrefix = nestedHeading ? `${nestedHeading} ` : null; + + const extractTypeFromLine = (line) => { + if (!line) return null; + const trimmed = line.trim(); + if (!trimmed) return null; + if (trimmed.startsWith('>')) { + // Handle lines like: > **entities**: `object` or > **auth**: [`AuthMethods`](../interfaces/AuthMethods) + const blockMatch = trimmed.match(/^>\s*\*\*([^*]+)\*\*:\s*(.+)$/); + if (blockMatch) { + const typePart = blockMatch[2].replace(/`/g, '').trim(); + // Check if it's a markdown link: [TypeName](link) + const linkMatch = typePart.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (linkMatch) { + return { type: linkMatch[1], link: linkMatch[2] }; + } + return { type: typePart, link: null }; + } + } + if (trimmed.includes('`')) { + // Extract type from backticks, could be a link: [`AuthMethods`](../interfaces/AuthMethods) + const typeMatch = trimmed.match(/`([^`]+)`/); + if (typeMatch) { + const typePart = typeMatch[1].trim(); + // Check if there's a link after the backticks + const linkMatch = trimmed.match(/`[^`]+`\s*\[([^\]]+)\]\(([^)]+)\)/); + if (linkMatch) { + return { type: linkMatch[1], link: linkMatch[2] }; + } + // Check if the type itself is a link format + const inlineLinkMatch = typePart.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (inlineLinkMatch) { + return { type: inlineLinkMatch[1], link: inlineLinkMatch[2] }; + } + return { type: typePart, link: null }; + } + } + // Check for standalone markdown links + const linkMatch = trimmed.match(/^\[([^\]]+)\]\(([^)]+)\)$/); + if (linkMatch) { + return { type: linkMatch[1], link: linkMatch[2] }; + } + return null; + }; + + const isHeadingLine = (line) => headingPrefix && line.startsWith(headingPrefix); + const isNestedHeadingLine = (line) => nestedPrefix && line.startsWith(nestedPrefix); + + const leadingLines = []; + let index = 0; + if (headingPrefix) { + while (index < lines.length && !isHeadingLine(lines[index])) { + if (lines[index].trim()) { + leadingLines.push(lines[index]); + } + index++; + } + } + + // If no field headings found, treat as simple return + if (!headingPrefix || index >= lines.length) { + let type = returnTypeFromSignature || 'any'; + let typeLink = null; + const descriptionLines = []; + + // Check if there's an existing ResponseField in the content + const responseFieldMatch = sectionContent.match(/]*type="([^"]+)"[^>]*>/); + if (responseFieldMatch) { + // Extract type from existing ResponseField + const existingType = responseFieldMatch[1]; + if (existingType && existingType !== 'any') { + type = existingType; + } + } + + for (const line of lines) { + // Skip ResponseField tags - we'll replace them + if (line.trim().startsWith('') { + continue; + } + const maybeType = extractTypeFromLine(line); + if (maybeType && type === 'any') { + if (typeof maybeType === 'object') { + type = maybeType.type; + typeLink = maybeType.link; + } else { + type = maybeType; + } + continue; + } + if (line.trim() && !line.trim().startsWith('`') && !line.trim().startsWith('<')) { + descriptionLines.push(line); + } + } + let description = descriptionLines.join('\n').trim(); + + // Check if we have a linked type to inline + let typeInfoToUse = linkedTypeInfo; + + // If we don't have linkedTypeInfo but we have a type name, try to resolve it + if (!typeInfoToUse && type && context && context.app) { + const simpleTypeName = getSimpleTypeName(type); + if (simpleTypeName && !PRIMITIVE_TYPES.includes(simpleTypeName)) { + const typePath = resolveTypePath(simpleTypeName, context.app, context.currentPagePath); + if (typePath) { + typeInfoToUse = { typeName: simpleTypeName, typePath }; + } else if (simpleTypeName) { + // Even if we can't resolve the path, try with just the name + typeInfoToUse = { typeName: simpleTypeName, typePath: simpleTypeName }; + } + + // Track resolved linked type + if (typeInfoToUse && linkedTypeNames) { + linkedTypeNames.add(typeInfoToUse.typeName); + if (writeLinkedTypesFile) { + writeLinkedTypesFile(); + } + } + } + } + + if (typeInfoToUse && context) { + const properties = extractPropertiesFromLinkedType(typeInfoToUse, context); + if (properties.length > 0) { + // Return separate ResponseFields for each property (skip the default "result" field) + const resultFields = []; + + // Add a separate ResponseField for each property + for (const prop of properties) { + resultFields.push({ + name: prop.name, + type: prop.type, + description: prop.description, + optional: prop.optional, + nested: prop.nested || [] + }); + } + + return { + fields: resultFields, + leadingText: '', + extractedTypeName: typeInfoToUse.typeName // Pass the type name for display + }; + } + } + + // Add "See [TypeName](link)" to description if there's a type link + if (typeLink) { + if (description) { + description += '\n\nSee [' + type + '](' + typeLink + ')'; + } else { + description = 'See [' + type + '](' + typeLink + ')'; + } + } + // Use 'result' as default name, or extract from description + let name = 'result'; + if (description) { + // Check if description contains a type hint + const typeHint = description.match(/(\w+)\s+(?:instance|object|value)/i); + if (typeHint) { + name = typeHint[1].toLowerCase(); + } + } + return { + fields: [ + { + name, + type, + description, + optional: false, + nested: [], + }, + ], + leadingText: '', + extractedTypeName: null, + }; + } + + // Parse fields with headings + while (index < lines.length) { + const headingLine = lines[index]; + if (!isHeadingLine(headingLine)) { + index++; + continue; + } + + let rawName = headingLine.slice(headingPrefix.length).trim(); + const optional = rawName.endsWith('?'); + const name = optional ? rawName.slice(0, -1).trim() : rawName.trim(); + index++; + + while (index < lines.length && lines[index].trim() === '') { + index++; + } + + let type = 'any'; + let typeLink = null; + if (index < lines.length) { + const maybeType = extractTypeFromLine(lines[index]); + if (maybeType) { + if (typeof maybeType === 'object') { + type = maybeType.type; + typeLink = maybeType.link; + } else { + type = maybeType; + } + index++; + } + } + + while (index < lines.length && lines[index].trim() === '') { + index++; + } + + const descriptionLines = []; + const nested = []; + + // Collect description and nested fields + while ( + index < lines.length && + !isHeadingLine(lines[index]) && + !(nestedPrefix && isNestedHeadingLine(lines[index])) + ) { + descriptionLines.push(lines[index]); + index++; + } + + // Parse nested fields if any + while (index < lines.length && isNestedHeadingLine(lines[index])) { + const nestedHeadingLine = lines[index]; + let nestedRawName = nestedHeadingLine.slice(nestedPrefix.length).trim(); + const nestedOptional = nestedRawName.endsWith('?'); + const nestedName = nestedOptional ? nestedRawName.slice(0, -1).trim() : nestedRawName.trim(); + index++; + + while (index < lines.length && lines[index].trim() === '') { + index++; + } + + let nestedType = 'any'; + let nestedTypeLink = null; + if (index < lines.length) { + const maybeNestedType = extractTypeFromLine(lines[index]); + if (maybeNestedType) { + if (typeof maybeNestedType === 'object') { + nestedType = maybeNestedType.type; + nestedTypeLink = maybeNestedType.link; + } else { + nestedType = maybeNestedType; + } + index++; + } + } + + while (index < lines.length && lines[index].trim() === '') { + index++; + } + + const nestedDescLines = []; + while ( + index < lines.length && + !isNestedHeadingLine(lines[index]) && + !isHeadingLine(lines[index]) + ) { + nestedDescLines.push(lines[index]); + index++; + } + + // Add "See [TypeName](link)" to nested description if there's a type link + let nestedDescription = nestedDescLines.join('\n').trim(); + if (nestedTypeLink) { + if (nestedDescription) { + nestedDescription += '\n\nSee [' + nestedType + '](' + nestedTypeLink + ')'; + } else { + nestedDescription = 'See [' + nestedType + '](' + nestedTypeLink + ')'; + } + } + + nested.push({ + name: nestedName, + type: nestedType, + description: nestedDescription, + optional: nestedOptional, + }); + } + + // Add "See [TypeName](link)" to description if there's a type link + let description = descriptionLines.join('\n').trim(); + if (typeLink) { + if (description) { + description += '\n\nSee [' + type + '](' + typeLink + ')'; + } else { + description = 'See [' + type + '](' + typeLink + ')'; + } + } + + fields.push({ + name, + type, + description, + optional, + nested, + }); + } + + return { fields, leadingText: leadingLines.join('\n').trim(), extractedTypeName: null }; +} + +function buildResponseFieldsSection(fields, linkedTypeNames = null, writeLinkedTypesFile = null) { + let output = ''; + + const PRIMITIVE_TYPES = ['any', 'string', 'number', 'boolean', 'void', 'null', 'undefined', 'object', 'Array', 'Promise']; + + for (const field of fields) { + const requiredAttr = field.optional ? '' : ' required'; + const defaultAttr = field.default ? ` default="${escapeAttribute(field.default)}"` : ''; + + // Track non-primitive return field types for suppression + if (linkedTypeNames && field.type && !PRIMITIVE_TYPES.includes(field.type)) { + const simpleTypeName = field.type.replace(/[<>\[\]]/g, '').trim(); + if (simpleTypeName && !PRIMITIVE_TYPES.includes(simpleTypeName)) { + linkedTypeNames.add(simpleTypeName); + if (writeLinkedTypesFile) { + writeLinkedTypesFile(); + } + } + } + + output += `\n`; + + if (field.description) { + output += `\n${field.description}\n`; + } + + if (field.nested && field.nested.length > 0) { + // Wrap nested fields in an Accordion component + output += `\n\n\n`; + + for (const nested of field.nested) { + const requiredAttr = nested.optional ? '' : ' required'; + output += `\n`; + + if (nested.description) { + output += `\n${nested.description}\n`; + } + + output += '\n\n\n'; + } + + output += '\n'; + } + + output += '\n\n\n'; + } + + return output; +} + +function formatReturnFieldsOutput(fields, returnType = null, linkedTypeNames = null, writeLinkedTypesFile = null) { + if (!fields || fields.length === 0) { + return ''; + } + + const fieldsBlock = buildResponseFieldsSection(fields, linkedTypeNames, writeLinkedTypesFile).trimEnd(); + if (!fieldsBlock) { + return ''; + } + + const hasMultipleFields = fields.length > 1; + const hasNestedFields = fields.some( + (field) => Array.isArray(field.nested) && field.nested.length > 0 + ); + + if (hasMultipleFields || hasNestedFields) { + // Extract the simple type name to display above the Accordion + let typeDisplay = ''; + if (returnType) { + const simpleTypeName = getSimpleTypeName(returnType); + if (simpleTypeName && !PRIMITIVE_TYPES.includes(simpleTypeName)) { + typeDisplay = `\`${simpleTypeName}\`\n\n`; + } + } + // If we still don't have a type display and have multiple fields, + // try to infer from the context (e.g., if all fields are from the same type) + if (!typeDisplay && hasMultipleFields && fields.length > 0) { + // Check if we can get a type hint from the first field's description or context + // This is a fallback for cases where returnType wasn't passed correctly + } + return `${typeDisplay}\n\n${fieldsBlock}\n`; + } + + return fieldsBlock; +} + +function getSimpleTypeName(typeName) { + if (!typeName) { + return null; + } + + // Remove generic arguments if they are still present (e.g., Promise) + const withoutGenerics = typeName.split('<')[0].trim(); + + // Type names can include dots for namespaces, so allow those + const match = withoutGenerics.match(/^[A-Za-z0-9_.]+$/); + return match ? match[0] : null; +} + diff --git a/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-utils.js b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-utils.js new file mode 100644 index 0000000..497bf79 --- /dev/null +++ b/scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-utils.js @@ -0,0 +1,12 @@ +/** + * Utility functions for TypeDoc Mintlify plugin + */ + +/** + * Escape special characters for use in HTML attributes + */ +export function escapeAttribute(value) { + return String(value).replace(/"/g, '"'); +} + + diff --git a/scripts/mintlify-post-processing/types-to-expose.json b/scripts/mintlify-post-processing/types-to-expose.json new file mode 100644 index 0000000..42f0c15 --- /dev/null +++ b/scripts/mintlify-post-processing/types-to-expose.json @@ -0,0 +1,12 @@ +[ + "AgentsModule", + "AppLogsModule", + "AuthModule", + "ConnectorsModule", + "EntitiesModule", + "FunctionsModule", + "IntegrationsModule", + "SsoModule" +] + + diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..b4a7259 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts"], + "out": "docs/content", + "plugin": [ + "typedoc-plugin-markdown", + "./scripts/mintlify-post-processing/typedoc-plugin/typedoc-mintlify-plugin.js" + ], + "fileExtension": ".mdx", + "excludePrivate": true, + "excludeProtected": true, + "excludeInternal": true, + "excludeExternals": true, + "readme": "none", + "gitRevision": "main", + "sort": ["source-order"], + "kindSortOrder": [ + "Project", + "Module", + "Namespace", + "Enum", + "Class", + "Interface", + "TypeAlias", + "Constructor", + "Property", + "Method", + "Function", + "Accessor", + "Variable" + ], + "entryFileName": "README.mdx", + "maxTypeConversionDepth": 2, + "hideBreadcrumbs": true, + "disableSources": true, + "hidePageTitle": true +} diff --git a/writing-docs.md b/writing-docs.md new file mode 100644 index 0000000..4f7a183 --- /dev/null +++ b/writing-docs.md @@ -0,0 +1,34 @@ +# SDK Documentation + +Documentation for the SDK is generated using TypeDoc. The TypeDoc files are post-processed to convert them to Mintlify format. You can preview the output locally, and then push it to the docs repo to delpoy it. + +## Before getting started +* Install the repo dependencies: `npm install` +* Install the Mintlify CLI: `npm i -g mint` + +## Generate docs +Open the terminal in the repo and run `npm run create-docs`. The docs files appear under `/docs/content`. + +## Preview docs locally with Mintlify +1. In the terminal, navigate to the `docs` folder. +1. Run `mint dev`. The docs preview opens in your browser. + +> If you notice that the names appearing for the sections of the docs menu aren't right, you may need to adjust `scripts/mintlify-post-processing/category-map.json`. This file maps the names of the subfolders in `/docs/content` to the desired section names in the reference. + +### Category mapping +`scripts/mintlify-post-processing/category-map.json` maps the names of the output folders from TypeDoc to the names of the categories that you want to appear in the docs. Only folder names that are mapped in this file appear in the final docs and the local preview. + +For example, if you map `interfaces` to `Modules`, the files in `docs/content/interfaces` appear in the docs under a **Modules** category. + +The names of the TypeDoc output folders are: `classes`, `functions`, `interfaces`, `type-aliases`. + +## Control which types appear in the docs +`scripts/mintlify-post-processing/types-to-expose.json` lists the TypeDoc types that the post-processing script keeps in the generated reference. Add or remove type names in that file to expose different SDK areas (for example, to surface a new type or hide one that is not ready for publication). After editing the list, rerun `npm run create-docs` so the Mintlify-ready content reflects the updated exposure set. + +## Push SDK docs to the Mintlify docs repository + +After generating and reviewing the docs, you can push them to the `base44/mintlify-docs` repo to deploy them. + +1. In the terminal, run `npm run push-docs -- --branch `. If the branch already exists, your changes are added to the ones already on the branch. Otherwise, the script creates a new branch with the chosen name. +1. Open the [docs repo](https://github.com/base44-dev/mintlify-docs) and created a PR for your branch. +1. Preview your docs using the [Mintlify dashboard](https://dashboard.mintlify.com/base44/base44?section=previews). \ No newline at end of file