/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import path from 'node:path'; import {fileURLToPath} from 'node:url'; import {escapeTableText, formatDefault, readJsonFile, toAnchor, wrapCode, writeFile} from './shared.mjs'; function buildDefMap(schema) { return schema.$defs ?? {}; } function formatType(propSchema, defs) { if (!propSchema || typeof propSchema !== 'object') { return 'unknown'; } if (propSchema.$ref) { const refName = propSchema.$ref.split('/').pop(); return `[${refName}](#${toAnchor(refName)})`; } if (propSchema.enum) { const values = propSchema.enum.map((v) => wrapCode(v)).join(', '); return `enum<${values}>`; } if (propSchema.type === 'array') { const items = propSchema.items; if (items) { const itemType = formatType(items, defs); return `array<${itemType}>`; } return 'array'; } if (propSchema.type === 'object') { if (propSchema.properties && Object.keys(propSchema.properties).length > 0) { return 'object'; } if (propSchema.additionalProperties) { return 'object'; } return 'object'; } if (propSchema.type) { return propSchema.type; } return 'unknown'; } /** * Format field name with optional indicator. * Appends ? suffix when the field is not required. */ function formatFieldName(name, isRequired) { return isRequired ? name : `${name}?`; } /** * Build description with default value if present. */ function buildDescription(propSchema) { let desc = escapeTableText(propSchema.description ?? ''); const defaultVal = formatDefault(propSchema.default); if (defaultVal) { if (desc) { desc += ` Default: ${defaultVal}`; } else { desc = `Default: ${defaultVal}`; } } return desc; } function renderPropertyTable(properties, requiredSet, defs) { if (!properties || Object.keys(properties).length === 0) { return ''; } const propNames = Object.keys(properties).sort((a, b) => a.localeCompare(b)); let out = ''; out += '| Property | Type | Description |\n'; out += '|----------|------|-------------|\n'; for (const propName of propNames) { const propSchema = properties[propName]; const type = formatType(propSchema, defs); const isRequired = requiredSet.has(propName); const fieldName = formatFieldName(propName, isRequired); const description = buildDescription(propSchema); out += `| ${fieldName} | ${type} | ${description} |\n`; } out += '\n'; return out; } function renderConditionalNote(schema) { if (!schema.if || !schema.then) { return ''; } let out = ''; const condition = schema.if; const thenClause = schema.then; const conditionParts = new Set(); const keysWithExplicitCondition = new Set(); if (condition.properties) { for (const [key, val] of Object.entries(condition.properties)) { if (val.const !== undefined) { conditionParts.add(`${wrapCode(key)} = ${wrapCode(val.const)}`); keysWithExplicitCondition.add(key); continue; } if (val.properties) { for (const [subKey, subVal] of Object.entries(val.properties)) { if (subVal.const !== undefined) { conditionParts.add(`${wrapCode(`${key}.${subKey}`)} = ${wrapCode(subVal.const)}`); keysWithExplicitCondition.add(key); } } } } } if (condition.required) { for (const requiredKey of condition.required) { if (!keysWithExplicitCondition.has(requiredKey)) { conditionParts.add(`${wrapCode(requiredKey)} is present`); } } } const conditionText = Array.from(conditionParts).join(' and '); let requiredProps = []; if (thenClause.required) { requiredProps = thenClause.required; } if (thenClause.properties) { for (const [key, val] of Object.entries(thenClause.properties)) { if (val.required) { for (const req of val.required) { requiredProps.push(`${key}.${req}`); } } } } if (conditionText && requiredProps.length > 0) { const requiredList = requiredProps.map((p) => wrapCode(p)).join(', '); out += `\nWhen ${conditionText}, the following properties are required: ${requiredList}\n\n\n`; } return out; } function renderJsonExample(_sectionPath, properties, requiredSet, _defs) { if (!properties || Object.keys(properties).length === 0) { return ''; } const exampleObj = {}; const propNames = Object.keys(properties).sort((a, b) => { const aRequired = requiredSet.has(a) ? 0 : 1; const bRequired = requiredSet.has(b) ? 0 : 1; if (aRequired !== bRequired) return aRequired - bRequired; return a.localeCompare(b); }); for (const propName of propNames) { const propSchema = properties[propName]; if (propSchema.$ref) { continue; } if (propSchema.type === 'object' && propSchema.properties) { continue; } const isRequired = requiredSet.has(propName); let exampleValue; if (propSchema.default !== undefined) { exampleValue = propSchema.default; } else if (propSchema.enum) { exampleValue = propSchema.enum[0]; } else if (propSchema.type === 'string') { exampleValue = isRequired ? `your_${propName}` : ''; } else if (propSchema.type === 'number') { exampleValue = 0; } else if (propSchema.type === 'boolean') { exampleValue = false; } else if (propSchema.type === 'array') { exampleValue = []; } else { continue; } exampleObj[propName] = exampleValue; } if (Object.keys(exampleObj).length === 0) { return ''; } const jsonStr = JSON.stringify(exampleObj, null, 2); let out = '\n'; out += '```json\n'; out += jsonStr; out += '\n```\n'; out += '\n\n'; return out; } function renderDefinition(defName, defSchema, defs, jsonPath) { let out = ''; out += `### ${defName}\n\n`; if (jsonPath) { out += `JSON path: ${wrapCode(jsonPath)}\n\n`; } if (defSchema.description) { out += `${defSchema.description}\n\n`; } const requiredSet = new Set(defSchema.required ?? []); out += renderConditionalNote(defSchema); if (defSchema.properties) { out += renderPropertyTable(defSchema.properties, requiredSet, defs); out += renderJsonExample(jsonPath, defSchema.properties, requiredSet, defs); } return out; } function renderNestedDefinitions(_defName, defSchema, defs, parentPath, rendered) { let out = ''; if (!defSchema.properties) { return out; } for (const [propName, propSchema] of Object.entries(defSchema.properties)) { if (propSchema.$ref) { const refName = propSchema.$ref.split('/').pop(); if (rendered.has(refName)) { continue; } const refSchema = defs[refName]; if (refSchema) { const nestedPath = parentPath ? `${parentPath}.${propName}` : propName; rendered.add(refName); out += renderDefinition(refName, refSchema, defs, nestedPath); out += renderNestedDefinitions(refName, refSchema, defs, nestedPath, rendered); } } else if (propSchema.type === 'object' && propSchema.properties) { const _nestedPath = parentPath ? `${parentPath}.${propName}` : propName; const syntheticName = propName; out += `#### ${syntheticName}\n\n`; if (propSchema.description) { out += `${propSchema.description}\n\n`; } const requiredSet = new Set(propSchema.required ?? []); out += renderPropertyTable(propSchema.properties, requiredSet, defs); } } return out; } function renderTableOfContents(schema, _defs) { let out = '## Table of contents\n\n'; out += '**Root configuration**\n\n'; out += '- [Root properties](#root-properties)\n'; out += '\n**Sections**\n\n'; const rootProps = schema.properties ?? {}; const sections = []; for (const [_propName, propSchema] of Object.entries(rootProps)) { if (propSchema.$ref) { const refName = propSchema.$ref.split('/').pop(); sections.push({name: refName, anchor: toAnchor(refName)}); } } sections.sort((a, b) => a.name.localeCompare(b.name)); for (const section of sections) { out += `- [${section.name}](#${section.anchor})\n`; } out += '\n'; out += '## Field notation\n\n'; out += 'Configuration tables use a compact notation:\n\n'; out += '| Notation | Meaning |\n'; out += '|----------|----------|\n'; out += '| `property` | Required property |\n'; out += '| `property?` | Optional property (may be omitted) |\n'; out += '\n'; out += 'Default values are shown in the Description column when applicable.\n\n'; return out; } function renderRootProperties(schema, defs) { let out = '## Root properties\n\n'; const properties = schema.properties ?? {}; const requiredSet = new Set(schema.required ?? []); out += 'These are the top-level configuration options in your `config.json`.\n\n'; out += renderPropertyTable(properties, requiredSet, defs); out += renderConditionalNote(schema); return out; } function renderMdx(schema) { const defs = buildDefMap(schema); let out = ''; out += '---\n'; out += "title: 'Configuration'\n"; out += "description: 'config.json reference for self-hosted Fluxer.'\n"; out += '---\n\n'; out += renderTableOfContents(schema, defs); out += renderRootProperties(schema, defs); const rendered = new Set(); const rootProps = schema.properties ?? {}; const sortedProps = Object.entries(rootProps).sort(([a], [b]) => a.localeCompare(b)); for (const [propName, propSchema] of sortedProps) { if (propSchema.$ref) { const refName = propSchema.$ref.split('/').pop(); if (rendered.has(refName)) { continue; } const refSchema = defs[refName]; if (refSchema) { rendered.add(refName); out += `---\n\n`; out += `## ${refName}\n\n`; out += `\n\n`; out += `JSON path: ${wrapCode(propName)}\n\n`; if (refSchema.description) { out += `${refSchema.description}\n\n`; } const requiredSet = new Set(refSchema.required ?? []); out += renderConditionalNote(refSchema); if (refSchema.properties) { out += renderPropertyTable(refSchema.properties, requiredSet, defs); out += renderJsonExample(propName, refSchema.properties, requiredSet, defs); } out += renderNestedDefinitions(refName, refSchema, defs, propName, rendered); } } } return out; } async function main() { const dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(dirname, '../..'); const schemaPath = path.join(repoRoot, 'packages/config/src/ConfigSchema.json'); const outPath = path.join(repoRoot, 'fluxer_docs/self_hosting/configuration.mdx'); const schema = await readJsonFile(schemaPath); const mdx = renderMdx(schema); await writeFile(outPath, mdx); console.log(`Generated configuration documentation at ${outPath}`); } await main();