quine-linux/installer/anaconda-webui/vendor/docs/generate-docs.js
Philip Peterson 255042fe8d Vendor anaconda-webui and build it into the installer image
Replaces installing a patched anaconda-webui RPM from the forge's package
registry: installer/anaconda-webui/vendor/ is upstream release 86 verbatim
(see its PROVENANCE.md), and installer/Containerfile builds it with
upstream's own `make install` in a native-platform stage, then copies the
result over stock Fedora anaconda-webui. The produced file set matches
Fedora's anaconda-webui-86-1.fc46 exactly. Edits to the installer UI now
happen in vendor/ and ship on the next `task build-installer-iso`, with no
RPM, registry or CI in between.

Also follows bootc-image-builder's rename of --bootc-installer-payload-ref
to --installer-payload-ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 05:56:26 -08:00

416 lines
13 KiB
JavaScript
Executable file

#!/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();