#!/usr/bin/env node "use strict"; /** * Post-Process Specs * * Applies content overlays and tag configuration to bundled OpenAPI specs. * Runs after `getswagger.sh` bundles specs and before * `generate-openapi-articles.ts` generates Hugo pages. * * Replaces Redocly decorators for: * - info.yml overlays (title, description, version, license, contact, x-* fields) * - servers.yml overlays (replaces spec.servers array) * - tags.yml config (rename, describe, add x-related links to tags) * * Usage: * node api-docs/scripts/dist/post-process-specs.js # All products * node api-docs/scripts/dist/post-process-specs.js influxdb3/core # One product * * @module post-process-specs */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("fs")); const path = __importStar(require("path")); const yaml = __importStar(require("js-yaml")); // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- const LOG_PREFIX = '[post-process]'; /** Build output directory for resolved specs. Source specs are never mutated. */ const BUILD_DIR = '_build'; /** Product directories that contain a .config.yml with `apis:` entries. */ const PRODUCT_DIRS = [ 'influxdb3/core', 'influxdb3/enterprise', 'influxdb3/cloud-dedicated', 'influxdb3/cloud-serverless', 'influxdb3/clustered', 'influxdb/cloud', 'influxdb/v2', 'influxdb/v1', 'enterprise_influxdb/v1', ]; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Parse a YAML file and return the parsed object, or null if the file does * not exist. */ function loadYaml(filePath) { if (!fs.existsSync(filePath)) return null; const raw = fs.readFileSync(filePath, 'utf8'); return yaml.load(raw); } /** * Write an object to a YAML file. */ function writeYaml(filePath, data) { fs.writeFileSync(filePath, yaml.dump(data, { lineWidth: -1 }), 'utf8'); } function log(msg) { process.stderr.write(`${LOG_PREFIX} ${msg}\n`); } // --------------------------------------------------------------------------- // Content overlays // --------------------------------------------------------------------------- /** * Resolve a content file path using the same convention as the Redocly * docs-content.cjs helper: try API-specific directory first, fall back to * product-level directory. * * @param filename - e.g. 'info.yml' or 'servers.yml' * @param specDir - Absolute path to the directory containing the spec file. * @param productAbsDir - Absolute path to the product directory. * @returns Absolute path to the content file, or null if not found. */ function resolveContentFile(filename, specDir, productAbsDir) { // API-specific: {specDir}/content/{filename} const apiSpecific = path.join(specDir, 'content', filename); if (fs.existsSync(apiSpecific)) return apiSpecific; // Product-level fallback: {productAbsDir}/content/{filename} const productLevel = path.join(productAbsDir, 'content', filename); if (fs.existsSync(productLevel)) return productLevel; return null; } /** * Apply info.yml overlay to the spec. Merges each field present in the * overlay into spec.info, preserving fields not mentioned in the overlay. * * @returns true if any fields were applied. */ function applyInfoOverlay(spec, specDir, productAbsDir, label) { const infoPath = resolveContentFile('info.yml', specDir, productAbsDir); if (!infoPath) return false; const overlay = loadYaml(infoPath); if (!overlay) return false; if (!spec.info) spec.info = {}; let applied = 0; for (const [key, value] of Object.entries(overlay)) { spec.info[key] = value; applied++; } if (applied > 0) { log(`${label}: applied ${applied} info field(s) from ${path.relative(productAbsDir, infoPath)}`); } return applied > 0; } /** * Apply servers.yml overlay to the spec. Replaces spec.servers entirely. * * @returns true if servers were applied. */ function applyServersOverlay(spec, specDir, productAbsDir, label) { const serversPath = resolveContentFile('servers.yml', specDir, productAbsDir); if (!serversPath) return false; const servers = loadYaml(serversPath); if (!servers || !Array.isArray(servers)) return false; spec.servers = servers; log(`${label}: applied ${servers.length} server(s) from ${path.relative(productAbsDir, serversPath)}`); return true; } // --------------------------------------------------------------------------- // Tag config // --------------------------------------------------------------------------- /** * Collect every tag name referenced across all operations in the spec. */ function collectOperationTags(spec) { const found = new Set(); for (const pathItem of Object.values(spec.paths ?? {})) { for (const operation of Object.values(pathItem)) { if (operation && typeof operation === 'object' && Array.isArray(operation.tags)) { for (const t of operation.tags) found.add(t); } } } return found; } /** * Rename a tag throughout the spec: in `tags[]` and in every operation. */ function renameTag(spec, oldName, newName) { for (const tag of spec.tags ?? []) { if (tag.name === oldName) tag.name = newName; } for (const pathItem of Object.values(spec.paths ?? {})) { for (const operation of Object.values(pathItem)) { if (operation && typeof operation === 'object' && Array.isArray(operation.tags)) { operation.tags = operation.tags.map((t) => t === oldName ? newName : t); } } } } /** * Apply tag config from a `tags.yml` file to the spec. * * @returns true if any tags were patched. */ function applyTagConfig(spec, tagConfigPath, label) { const tagsCfg = loadYaml(tagConfigPath); if (!tagsCfg || !tagsCfg.tags) { log(`${label}: tags.yml has no 'tags' key — skipping`); return false; } if (!Array.isArray(spec.tags)) spec.tags = []; const operationTags = collectOperationTags(spec); const configKeys = Object.keys(tagsCfg.tags); // Warn: config references a tag not in the spec for (const cfgKey of configKeys) { const effectiveName = tagsCfg.tags[cfgKey]?.rename ?? cfgKey; if (!operationTags.has(cfgKey) && !operationTags.has(effectiveName)) { log(`WARN ${label}: config tag '${cfgKey}' not found in spec operations`); } } // Warn: spec has operation tags with no config entry Array.from(operationTags).forEach((opTag) => { const hasEntry = configKeys.some((k) => k === opTag || tagsCfg.tags[k]?.rename === opTag); if (!hasEntry) { log(`WARN ${label}: spec tag '${opTag}' has no config entry in tags.yml`); } }); // Apply transformations for (const [tagKey, cfg] of Object.entries(tagsCfg.tags)) { if (cfg.rename) { log(`${label}: renaming tag '${tagKey}' → '${cfg.rename}'`); renameTag(spec, tagKey, cfg.rename); } const resolvedName = cfg.rename ?? tagKey; let tagObj = spec.tags.find((t) => t.name === resolvedName); if (!tagObj) { tagObj = { name: resolvedName }; spec.tags.push(tagObj); } if (cfg.description !== undefined) tagObj.description = cfg.description.trim(); if (cfg['x-traitTag'] !== undefined) tagObj['x-traitTag'] = cfg['x-traitTag']; if (cfg['x-related'] !== undefined) tagObj['x-related'] = cfg['x-related']; } log(`${label}: patched ${configKeys.length} tag(s)`); return true; } // --------------------------------------------------------------------------- // Core logic // --------------------------------------------------------------------------- /** * Process a single product directory: read `.config.yml`, find spec files, * apply content overlays and tag configs, write resolved specs to _build/. * * Source specs in api-docs/ are never mutated. Resolved output goes to * api-docs/_build/{productDir}/{specFile} for downstream consumers * (Redoc HTML, generate-openapi-articles.ts). */ function processProduct(apiDocsRoot, productDir) { const productAbsDir = path.join(apiDocsRoot, productDir); const configPath = path.join(productAbsDir, '.config.yml'); const config = loadYaml(configPath); if (!config || !config.apis) { log(`${productDir}: no .config.yml or no 'apis' key — skipping`); return; } for (const [apiKey, apiEntry] of Object.entries(config.apis)) { const specRelPath = apiEntry.root; const specAbsPath = path.join(productAbsDir, specRelPath); const specDir = path.join(productAbsDir, path.dirname(specRelPath)); const label = path.join(productDir, specRelPath); if (!fs.existsSync(specAbsPath)) { log(`${label}: spec not found at ${specAbsPath} — skipping`); continue; } // Load spec once const spec = loadYaml(specAbsPath); if (!spec) { log(`${label}: failed to parse spec — skipping`); continue; } // Apply all transforms applyInfoOverlay(spec, specDir, productAbsDir, label); applyServersOverlay(spec, specDir, productAbsDir, label); const tagConfigPath = path.join(specDir, 'tags.yml'); if (fs.existsSync(tagConfigPath)) { applyTagConfig(spec, tagConfigPath, label); } // Write resolved spec to _build/, mirroring the source path structure const outPath = path.join(apiDocsRoot, BUILD_DIR, productDir, specRelPath); const outDir = path.dirname(outPath); if (!fs.existsSync(outDir)) { fs.mkdirSync(outDir, { recursive: true }); } writeYaml(outPath, spec); log(`${label}: wrote ${path.relative(apiDocsRoot, outPath)}`); } } // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- function main() { const args = process.argv.slice(2); // Optional --root flag for testing — overrides the default resolution. let apiDocsRoot = path.resolve(__dirname, '../..'); // api-docs/scripts/dist -> api-docs/ let targetProduct; for (let i = 0; i < args.length; i++) { if (args[i] === '--root' && args[i + 1]) { apiDocsRoot = path.resolve(args[i + 1]); i++; } else { targetProduct = args[i]; } } const products = targetProduct ? [targetProduct] : PRODUCT_DIRS; let hasError = false; for (const productDir of products) { try { processProduct(apiDocsRoot, productDir); } catch (err) { log(`ERROR ${productDir}: ${err.message}`); hasError = true; } } process.exit(hasError ? 1 : 0); } main(); //# sourceMappingURL=post-process-specs.js.map