#!/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); // Mock cockpit dependency for Node.js environment global.cockpit = { gettext: (text) => text, locale: () => "en" }; // eslint-disable-next-line no-console const log = console.log; // eslint-disable-next-line no-console const warn = console.warn; // Utility function to find all index.js files in a directory recursively 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; } // Global mocking system - create mocked versions of all files once class MockingSystem { constructor () { this.mockedFiles = new Map(); this.tempDir = path.join(__dirname, ".temp-mocks"); } async initialize () { // Create temp directory if (fs.existsSync(this.tempDir)) { fs.rmSync(this.tempDir, { recursive: true }); } fs.mkdirSync(this.tempDir, { recursive: true }); // Mock all index.js files that import JSX await this.mockAllIndexFiles(); // Mock steps.js await this.mockStepsFile(); } cleanup () { if (fs.existsSync(this.tempDir)) { fs.rmSync(this.tempDir, { recursive: true }); } } async mockAllIndexFiles () { // Find all index.js files in component directories const srcDir = path.resolve(__dirname, "../src/components"); const indexFiles = findIndexFiles(srcDir); for (const filePath of indexFiles) { await this.createMockedFile(filePath); } } async mockScenarioFiles (scenarioFiles) { // Mock scenario files that aren't already mocked for (const filePath of scenarioFiles) { if (!this.mockedFiles.has(filePath)) { await this.createMockedFile(filePath); } } } async createMockedFile (originalPath) { let content = fs.readFileSync(originalPath, "utf8"); // Apply comprehensive mocking content = this.applyMocking(content); // Create relative path for temp file const relativePath = path.relative(path.resolve(__dirname, "../src"), originalPath); const mockedPath = path.join(this.tempDir, relativePath); // Ensure directory exists fs.mkdirSync(path.dirname(mockedPath), { recursive: true }); // Write mocked file fs.writeFileSync(mockedPath, content); // Store mapping this.mockedFiles.set(originalPath, mockedPath); return mockedPath; } async mockStepsFile () { const stepsPath = path.resolve(__dirname, "../src/components/steps.js"); let content = fs.readFileSync(stepsPath, "utf8"); // Apply basic mocking for steps.js content = this.applyMocking(content); // Update imports to point to mocked files content = content.replace( /import\s+\{\s*Page\s+as\s+([^}]+)\s*\}\s+from\s+["']([^"']+)["'];?/g, (match, pageName, importPath) => { // Convert relative import to absolute 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}";`; } ); // Write mocked steps.js const mockedStepsPath = path.join(this.tempDir, "components/steps.js"); fs.mkdirSync(path.dirname(mockedStepsPath), { recursive: true }); fs.writeFileSync(mockedStepsPath, content); return mockedStepsPath; } applyMocking (content) { // Mock cockpit import content = content.replace( /import\s+cockpit\s+from\s+["']cockpit["'];?/g, "const cockpit = { gettext: (text) => text, format: (str, ...args) => str };" ); // Mock debug import content = content.replace( /import\s+\{\s*debug\s*\}\s+from\s+["'][^"']*["'];?/g, "const debug = () => {};" ); // Mock all JSX file imports with comprehensive function mocks 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 => { // Create different types of mocks based on naming patterns 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; } } // Get step order from the getSteps function async function getOrderedSteps (mockingSystem) { const getSteps = await mockingSystem.importMockedSteps(); // Call getSteps with mock arguments const steps = getSteps({}, {}, {}); return steps .flat() .filter(step => step.id && step._description); } // Extract scenario information using the mocking system async function extractScenarioInfo (indexPath, mockingSystem) { if (!fs.existsSync(indexPath)) { return null; } const content = fs.readFileSync(indexPath, "utf8"); // Extract JSDoc comments const jsdocMatch = content.match(/\/\*\*\s*([\s\S]*?)\*\//); let description = null; if (jsdocMatch) { const jsdocContent = jsdocMatch[1]; // Extract description const descMatch = jsdocContent.match(/@description\s+(.*?)(?=@|$)/s); if (descMatch) { description = descMatch[1] .replace(/\s*\*\s*/g, " ") .replace(/\s+/g, " ") .trim(); } } // Use the pre-mocked file from the mocking system const module = await mockingSystem.importMockedScenario(indexPath); if (!module.scenario) { warn(`No scenario export found in ${indexPath}`); return null; } return { description, ...module.scenario, }; } // Extract page information using the mocking system async function extractPageInfo (filePath, mockingSystem) { // Use the pre-mocked file from the mocking system const module = await mockingSystem.importMockedScenario(filePath); // Check if this file has a Page class, and if so extract page info if (module.Page) { // Create instance to get properties (pass dummy args for constructors that expect them) const pageInstance = new module.Page(false, "erase-all"); const result = { description: pageInstance._description || null, ...pageInstance, }; return result; } return null; // No page definition in this file } // Generate dynamic flow overview from page data function generateFlowOverview (orderedSteps) { let overview = ""; orderedSteps.forEach((step, index) => { const stepNumber = index + 1; overview += `${stepNumber}. **${!step.isFinal ? step.label : "Installation progress"}**\n`; }); return overview; } // Generate the main documentation content function generateStepsRst (orderedSteps, scenarios) { const flowOverview = generateFlowOverview(orderedSteps); // Generate storage scenarios section 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`; } // Check if individual scenario image exists and include it 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 `; } // Add spacing between scenarios except for the last one 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`; // Add scenarios subsection for the Installation method step if (step.id === "anaconda-screen-method") { stepContent += `\n${scenariosSection}`; } return stepContent; }).join("\n")} `; return rstContent; } // Main function to generate documentation async function generateDocs () { const mockingSystem = new MockingSystem(); try { log("Starting documentation generation..."); log("Initializing mocking system..."); // Initialize the global mocking system await mockingSystem.initialize(); log("Getting ordered steps..."); // Get ordered steps from getSteps() const orderedSteps = await getOrderedSteps(mockingSystem); log("Processing scenarios and pages..."); // Get all scenarios and pages const scenariosDir = path.join(__dirname, "..", "src", "components", "storage", "scenarios"); const scenarioIndexFiles = findIndexFiles(scenariosDir); // Filter out the root scenarios/index.js file - only include files in subdirectories const scenarioFiles = scenarioIndexFiles.filter(filePath => { const relativePath = path.relative(scenariosDir, filePath); return relativePath !== "index.js"; // Exclude the root index.js }); // Mock scenario files before trying to import them await mockingSystem.mockScenarioFiles(scenarioFiles); const scenarioPromises = scenarioFiles.map(filePath => extractScenarioInfo(filePath, mockingSystem)); const scenarios = await Promise.all(scenarioPromises); // Find page files using findIndexFiles and filter out scenarios directory const componentsDir = path.resolve(__dirname, "../src/components"); const allIndexFiles = findIndexFiles(componentsDir); const pageFiles = allIndexFiles .filter(filePath => !filePath.startsWith(scenariosDir)); // Mock page files before trying to import them await mockingSystem.mockScenarioFiles(pageFiles); const pagePromises = pageFiles.map(filePath => extractPageInfo(filePath, mockingSystem)); const pages = await Promise.all(pagePromises); // Match pages with steps by ID const stepsWithPageData = orderedSteps.map(step => { const matchingPage = pages.find(page => page?.id === step.id); return { ...step, description: matchingPage?.description, }; }); log("Generating documentation..."); // Generate the documentation const stepsRst = generateStepsRst(stepsWithPageData, scenarios); // Write the documentation const outputPath = path.join(__dirname, "installation-steps.rst"); fs.writeFileSync(outputPath, stepsRst); log("Documentation generation completed!"); } finally { // Always cleanup temp files mockingSystem.cleanup(); } } // Run the documentation generation generateDocs();