#!/usr/bin/env node
* Auto-generate documentation for Anaconda storage scenarios
* This script extracts @description from JSDoc comments in scenario files
*/
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
global.cockpit = {
gettext: (text) => text,
locale: () => "en"
};
const log = console.log;
const warn = console.warn;
function findIndexFiles (dir) {
const files = [];
function traverse (currentDir) {
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
traverse(fullPath);
} else if (entry.name === "index.js") {
files.push(fullPath);
}
}
}
traverse(dir);
return files;
}
class MockingSystem {
constructor () {
this.mockedFiles = new Map();
this.tempDir = path.join(__dirname, ".temp-mocks");
}
async initialize () {
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true });
}
fs.mkdirSync(this.tempDir, { recursive: true });
await this.mockAllIndexFiles();
await this.mockStepsFile();
}
cleanup () {
if (fs.existsSync(this.tempDir)) {
fs.rmSync(this.tempDir, { recursive: true });
}
}
async mockAllIndexFiles () {
const srcDir = path.resolve(__dirname, "../src/components");
const indexFiles = findIndexFiles(srcDir);
for (const filePath of indexFiles) {
await this.createMockedFile(filePath);
}
}
async mockScenarioFiles (scenarioFiles) {
for (const filePath of scenarioFiles) {
if (!this.mockedFiles.has(filePath)) {
await this.createMockedFile(filePath);
}
}
}
async createMockedFile (originalPath) {
let content = fs.readFileSync(originalPath, "utf8");
content = this.applyMocking(content);
const relativePath = path.relative(path.resolve(__dirname, "../src"), originalPath);
const mockedPath = path.join(this.tempDir, relativePath);
fs.mkdirSync(path.dirname(mockedPath), { recursive: true });
fs.writeFileSync(mockedPath, content);
this.mockedFiles.set(originalPath, mockedPath);
return mockedPath;
}
async mockStepsFile () {
const stepsPath = path.resolve(__dirname, "../src/components/steps.js");
let content = fs.readFileSync(stepsPath, "utf8");
content = this.applyMocking(content);
content = content.replace(
/import\s+\{\s*Page\s+as\s+([^}]+)\s*\}\s+from\s+["']([^"']+)["'];?/g,
(match, pageName, importPath) => {
const absoluteImportPath = path.resolve(path.dirname(stepsPath), importPath);
const mockedPath = this.mockedFiles.get(absoluteImportPath);
const relativeToTemp = path.relative(path.dirname(path.join(this.tempDir, "components/steps.js")), mockedPath);
return `import { Page as ${pageName} } from "./${relativeToTemp}";`;
}
);
const mockedStepsPath = path.join(this.tempDir, "components/steps.js");
fs.mkdirSync(path.dirname(mockedStepsPath), { recursive: true });
fs.writeFileSync(mockedStepsPath, content);
return mockedStepsPath;
}
applyMocking (content) {
content = content.replace(
/import\s+cockpit\s+from\s+["']cockpit["'];?/g,
"const cockpit = { gettext: (text) => text, format: (str, ...args) => str };"
);
content = content.replace(
/import\s+\{\s*debug\s*\}\s+from\s+["'][^"']*["'];?/g,
"const debug = () => {};"
);
content = content.replace(
/import\s+\{\s*([^}]+)\s*\}\s+from\s+["'][^"']*\.jsx["'];?/g,
(match, componentNames) => {
const names = componentNames.split(",").map(name => name.trim())
.filter(name => name.length > 0);
const mocks = names.map(name => {
if (name.startsWith("use") || name.startsWith("get")) {
return `const ${name} = () => null;`;
} else {
return `const ${name} = {};`;
}
}).join("\n");
return mocks;
}
);
return content;
}
async importMockedSteps () {
const mockedStepsPath = path.join(this.tempDir, "components/steps.js");
const moduleUrl = "file://" + mockedStepsPath;
const module = await import(moduleUrl);
return module.getSteps;
}
async importMockedScenario (originalPath) {
const mockedPath = this.mockedFiles.get(originalPath);
if (!mockedPath) {
throw new Error(`No mocked version found for ${originalPath}`);
}
const moduleUrl = "file://" + mockedPath;
const module = await import(moduleUrl);
return module;
}
}
async function getOrderedSteps (mockingSystem) {
const getSteps = await mockingSystem.importMockedSteps();
const steps = getSteps({}, {}, {});
return steps
.flat()
.filter(step => step.id && step._description);
}
async function extractScenarioInfo (indexPath, mockingSystem) {
if (!fs.existsSync(indexPath)) {
return null;
}
const content = fs.readFileSync(indexPath, "utf8");
const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//);
let description = null;
if (jsdocMatch) {
const jsdocContent = jsdocMatch[1];
const descMatch = jsdocContent.match(/@description\s+(.*?)(?=@|$)/s);
if (descMatch) {
description = descMatch[1]
.replace(/\s*\*\s*/g, " ")
.replace(/\s+/g, " ")
.trim();
}
}
const module = await mockingSystem.importMockedScenario(indexPath);
if (!module.scenario) {
warn(`No scenario export found in ${indexPath}`);
return null;
}
return {
description,
...module.scenario,
};
}
async function extractPageInfo (filePath, mockingSystem) {
const module = await mockingSystem.importMockedScenario(filePath);
if (module.Page) {
const pageInstance = new module.Page(false, "erase-all");
const result = {
description: pageInstance._description || null,
...pageInstance,
};
return result;
}
return null;
}
function generateFlowOverview (orderedSteps) {
let overview = "";
orderedSteps.forEach((step, index) => {
const stepNumber = index + 1;
overview += `${stepNumber}. **${!step.isFinal ? step.label : "Installation progress"}**\n`;
});
return overview;
}
function generateStepsRst (orderedSteps, scenarios) {
const flowOverview = generateFlowOverview(orderedSteps);
let scenariosSection = `
Storage Partitioning Scenarios
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Web UI installer provides several storage partitioning scenarios to accommodate different installation needs:
.. figure:: images/storage-scenario-overview.png
:width: 500px
:alt: Storage scenarios overview interface
`;
scenarios.forEach((scenario, index) => {
scenariosSection += `**${scenario.getLabel() || scenario.docsLabel}**\n\n`;
if (scenario.description) {
scenariosSection += `${scenario.description}\n`;
}
const scenarioImagePath = path.join(__dirname, "images", `storage-scenario-${scenario.id}.png`);
if (fs.existsSync(scenarioImagePath)) {
scenariosSection += `
.. figure:: images/storage-scenario-${scenario.id}.png
:width: 500px
:alt: ${scenario.getLabel() || scenario.docsLabel} scenario interface
`;
}
if (index < scenarios.length - 1) {
scenariosSection += "\n";
}
});
const rstContent = `
Installation Flow Overview
==========================
This document describes the step-by-step process for installing the system using the Web UI.
${flowOverview}
Detailed Step Descriptions
=============================
${orderedSteps.map((step, index) => {
const stepNumber = index + 1;
const stepTitle = `${stepNumber}. ${!step.isFinal ? step.label : "Installation progress"}`;
let stepContent = stepTitle + "\n" + "-".repeat(stepTitle.length) + `\n\n${step.description || "No description available."}\n`;
if (step.id === "anaconda-screen-method") {
stepContent += `\n${scenariosSection}`;
}
return stepContent;
}).join("\n")}
`;
return rstContent;
}
async function generateDocs () {
const mockingSystem = new MockingSystem();
try {
log("Starting documentation generation...");
log("Initializing mocking system...");
await mockingSystem.initialize();
log("Getting ordered steps...");
const orderedSteps = await getOrderedSteps(mockingSystem);
log("Processing scenarios and pages...");
const scenariosDir = path.join(__dirname, "..", "src", "components", "storage", "scenarios");
const scenarioIndexFiles = findIndexFiles(scenariosDir);
const scenarioFiles = scenarioIndexFiles.filter(filePath => {
const relativePath = path.relative(scenariosDir, filePath);
return relativePath !== "index.js";
});
await mockingSystem.mockScenarioFiles(scenarioFiles);
const scenarioPromises = scenarioFiles.map(filePath => extractScenarioInfo(filePath, mockingSystem));
const scenarios = await Promise.all(scenarioPromises);
const componentsDir = path.resolve(__dirname, "../src/components");
const allIndexFiles = findIndexFiles(componentsDir);
const pageFiles = allIndexFiles
.filter(filePath => !filePath.startsWith(scenariosDir));
await mockingSystem.mockScenarioFiles(pageFiles);
const pagePromises = pageFiles.map(filePath => extractPageInfo(filePath, mockingSystem));
const pages = await Promise.all(pagePromises);
const stepsWithPageData = orderedSteps.map(step => {
const matchingPage = pages.find(page => page?.id === step.id);
return {
...step,
description: matchingPage?.description,
};
});
log("Generating documentation...");
const stepsRst = generateStepsRst(stepsWithPageData, scenarios);
const outputPath = path.join(__dirname, "installation-steps.rst");
fs.writeFileSync(outputPath, stepsRst);
log("Documentation generation completed!");
} finally {
mockingSystem.cleanup();
}
}
generateDocs();