docs-v2/api-docs/scripts/dist/post-process-specs.js

334 lines
12 KiB
JavaScript

#!/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 <path> 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