docs-v2/helper-scripts/influxdb3-plugins/port_to_docs.js

572 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

#!/usr/bin/env node
/**
* Transforms plugin READMEs from influxdb3_plugins to docs-v2 format.
* Maintains consistency while applying documentation-specific enhancements.
*/
import { promises as fs } from 'fs';
import path from 'path';
import yaml from 'js-yaml';
/**
* Load the mapping configuration file.
*/
async function loadMappingConfig(configPath = 'docs_mapping.yaml') {
try {
const content = await fs.readFile(configPath, 'utf8');
return yaml.load(content);
} catch (error) {
if (error.code === 'ENOENT') {
console.error(`❌ Error: Configuration file '${configPath}' not found`);
} else {
console.error(`❌ Error parsing YAML configuration: ${error.message}`);
}
process.exit(1);
}
}
/**
* Remove the emoji metadata lines from content.
* Handles both single-line and multi-line formats:
* - Single: ⚡ scheduled 🔧 InfluxDB 3
* - Multi: ⚡ scheduled\n🏷 tags 🔧 InfluxDB 3
*/
function removeEmojiMetadata(content) {
// Remove multi-line emoji metadata (⚡ on first line, 🔧 on second line)
content = content.replace(/^⚡[^\n]*\n🏷[^\n]*🔧[^\n]*\n*/gm, '');
// Remove single-line emoji metadata (⚡ and 🔧 on same line)
content = content.replace(/^⚡.*?🔧.*?$\n*/gm, '');
return content;
}
/**
* Remove level 1 heading from content.
*/
function removeTitleHeading(content) {
// Title is in frontmatter, remove H1 from content
return content.replace(/^#\s+.+$\n*/m, '');
}
/**
* Remove standalone Description heading.
*/
function removeDescriptionHeading(content) {
// Remove "## Description" heading when it appears alone on a line
content = content.replace(/^##\s+Description\s*$/m, '');
return content;
}
/**
* Expand common abbreviations for readability.
*/
function expandAbbreviations(content) {
// Replace e.g., with "for example,"
content = content.replace(/\be\.g\.,\s*/g, 'for example, ');
// Replace i.e., with "that is,"
content = content.replace(/\bi\.e\.,\s*/g, 'that is, ');
return content;
}
/**
* Convert README TOML links to internal section links.
*/
function convertTomlReadmeLinks(content) {
// If document has TOML configuration section, link to it instead of external README
if (content.includes('## Using TOML Configuration Files')) {
content = content.replace(
/\[([^\]]*TOML[^\]]*)\]\(https:\/\/github\.com\/influxdata\/influxdb3_plugins\/blob\/master\/README\.md\)/gi,
'[$1](#using-toml-configuration-files)'
);
}
return content;
}
/**
* Convert relative links to GitHub URLs.
*/
function convertRelativeLinks(content, pluginName) {
const baseUrl = `https://github.com/influxdata/influxdb3_plugins/blob/master/influxdata/${pluginName}/`;
const rootUrl =
'https://github.com/influxdata/influxdb3_plugins/blob/master/';
// Convert relative README links (../../README.md, ../README.md, etc.)
content = content.replace(
/\[([^\]]+)\]\((\.\.\/)+README\.md\)/g,
`[$1](${rootUrl}README.md)`
);
// Convert TOML file links
content = content.replace(
/\[([^\]]+\.toml)\]\(\.?\/?([^)]+\.toml)\)/g,
(match, linkText, linkPath) => {
const cleanPath = linkPath.replace(/^\.\//, '');
return `[${linkText}](${baseUrl}${cleanPath})`;
}
);
// Convert Python file links
content = content.replace(
/\[([^\]]+\.py)\]\(\.?\/?([^)]+\.py)\)/g,
(match, linkText, linkPath) => {
const cleanPath = linkPath.replace(/^\.\//, '');
return `[${linkText}](${baseUrl}${cleanPath})`;
}
);
// Convert main README reference
content = content.replace(
'[influxdb3_plugins/README.md](/README.md)',
`[influxdb3_plugins/README.md](${rootUrl}README.md)`
);
return content;
}
/**
* Replace product references with Hugo shortcodes.
*/
function addProductShortcodes(content) {
// Replace various forms of InfluxDB 3 references
const replacements = [
[/InfluxDB 3 Core\/Enterprise/g, '{{% product-name %}}'],
[/InfluxDB 3 Core and InfluxDB 3 Enterprise/g, '{{% product-name %}}'],
[/InfluxDB 3 Core, InfluxDB 3 Enterprise/g, '{{% product-name %}}'],
// Be careful not to replace in URLs, code blocks, or product names like "InfluxDB 3 Explorer"
[/(?<!\/)InfluxDB 3(?! Explorer)(?![/_])/g, '{{% product-name %}}'],
];
for (const [pattern, replacement] of replacements) {
content = content.replace(pattern, replacement);
}
return content;
}
/**
* Optionally enhance the opening description for better SEO.
*/
function enhanceOpeningParagraph(content) {
// This is optional - the source description is usually sufficient
// Only enhance if needed for specific plugins
return content;
}
/**
* Add standard logging section if not present.
*/
function addLoggingSection(content) {
const loggingSection = `
## Logging
Logs are stored in the \`_internal\` database (or the database where the trigger is created) in the \`system.processing_engine_logs\` table. To view logs:
\`\`\`bash
influxdb3 query --database _internal "SELECT * FROM system.processing_engine_logs WHERE trigger_name = 'your_trigger_name'"
\`\`\`
Log columns:
- **event_time**: Timestamp of the log event
- **trigger_name**: Name of the trigger that generated the log
- **log_level**: Severity level (INFO, WARN, ERROR)
- **log_text**: Message describing the action or error`;
// Check if logging section already exists
if (!content.includes('## Logging')) {
// Insert before Questions/Comments section if it exists
if (content.includes('## Questions/Comments')) {
content = content.replace(
'## Questions/Comments',
loggingSection + '\n\n## Questions/Comments'
);
} else {
// Otherwise add before the end
content = content.trimEnd() + '\n' + loggingSection;
}
}
return content;
}
/**
* Replace Questions/Comments section with docs-v2 support sections.
*/
function replaceSupportSection(content) {
const supportSections = `## Report an issue
For plugin issues, see the Plugins repository [issues page](https://github.com/influxdata/influxdb3_plugins/issues).
## Find support for {{% product-name %}}
The [InfluxDB Discord server](https://discord.gg/9zaNCW2PRT) is the best place to find support for InfluxDB 3 Core and InfluxDB 3 Enterprise.
For other InfluxDB versions, see the [Support and feedback](#bug-reports-and-feedback) options.`;
// Remove existing Questions/Comments section
const pattern = /## Questions\/Comments.*?(?=\n##|\n\n##|$)/gs;
content = content.replace(pattern, '');
// Add new support sections
content = content.trimEnd() + '\n\n' + supportSections;
return content;
}
/**
* Extract style attributes from HTML comments and apply to headings.
* Converts: `#### Heading <!-- {.class} -->` to `#### Heading {.class}`
*
* Supported class formats:
* - {.green}, {.orange} - Color styling
* - {.recommended}, {.not-recommended} - Semantic styling
* - Any other {.classname} format
*
* This allows source READMEs to render cleanly on GitHub (which ignores
* HTML comments) while still supporting Hugo style classes in docs-v2.
*/
function extractStyleAttributes(content) {
// Match headings with HTML comment style attributes
// Pattern: (#+) (heading text) <!-- ({.classname}) -->
const pattern = /^(#{1,6})\s+(.+?)\s*<!--\s*(\{[^}]+\})\s*-->\s*$/gm;
return content.replace(pattern, '$1 $2 $3');
}
/**
* Ensure code blocks are properly formatted.
*/
function fixCodeBlockFormatting(content) {
// Add bash syntax highlighting where missing
content = content.replace(/```\n(influxdb3 |#)/g, '```bash\n$1');
// Ensure proper spacing around code blocks
content = content.replace(/```\n\n/g, '```\n');
return content;
}
/**
* Add schema requirements section for plugins that need it.
*/
function addSchemaRequirements(content, pluginName) {
// List of plugins that require schema information
const schemaPlugins = ['basic_transformation', 'downsampler'];
if (!schemaPlugins.includes(pluginName)) {
return content;
}
let schemaSection;
if (pluginName === 'basic_transformation') {
schemaSection = `## Schema requirements
The plugin assumes that the table schema is already defined in the database, as it relies on this schema to retrieve field and tag names required for processing.
> [!WARNING]
> #### Requires existing schema
>
> By design, the plugin returns an error if the schema doesn't exist or doesn't contain the expected columns.
`;
} else if (pluginName === 'downsampler') {
schemaSection = `## Schema management
Each downsampled record includes three additional metadata columns:
- \`record_count\` — the number of original points compressed into this single downsampled row
- \`time_from\` — the minimum timestamp among the original points in the interval
- \`time_to\` — the maximum timestamp among the original points in the interval
`;
} else {
return content;
}
// Insert after Configuration section
if (content.includes('## Installation steps')) {
content = content.replace(
'## Installation steps',
schemaSection + '\n## Installation steps'
);
}
return content;
}
/**
* Apply all transformations to convert README for docs-v2.
*/
function transformContent(content, pluginName, config) {
// Apply transformations in order
content = removeEmojiMetadata(content);
content = removeTitleHeading(content);
content = removeDescriptionHeading(content);
content = convertRelativeLinks(content, pluginName);
content = expandAbbreviations(content);
content = convertTomlReadmeLinks(content);
content = addProductShortcodes(content);
content = enhanceOpeningParagraph(content);
content = extractStyleAttributes(content);
content = fixCodeBlockFormatting(content);
// Add schema requirements if applicable
if (
config.additional_sections &&
config.additional_sections.includes('schema_requirements')
) {
content = addSchemaRequirements(content, pluginName);
}
// Add logging section
content = addLoggingSection(content);
// Replace support section
content = replaceSupportSection(content);
return content;
}
/**
* Process a single plugin README.
* Returns true if successful, false otherwise.
*/
async function processPlugin(pluginName, mapping, dryRun = false) {
const sourcePath = mapping.source;
const targetPath = mapping.target;
try {
// Check if source exists
await fs.access(sourcePath);
} catch (error) {
console.error(`❌ Source not found: ${sourcePath}`);
return false;
}
try {
// Read source content
const content = await fs.readFile(sourcePath, 'utf8');
// Transform content
const transformed = transformContent(content, pluginName, mapping);
if (dryRun) {
console.log(`✅ Would process ${pluginName}`);
console.log(` Source: ${sourcePath}`);
console.log(` Target: ${targetPath}`);
return true;
}
// Ensure target directory exists
await fs.mkdir(path.dirname(targetPath), { recursive: true });
// Write transformed content
await fs.writeFile(targetPath, transformed, 'utf8');
console.log(`✅ Processed ${pluginName}`);
console.log(` Source: ${sourcePath}`);
console.log(` Target: ${targetPath}`);
return true;
} catch (error) {
console.error(`❌ Error processing ${pluginName}: ${error.message}`);
return false;
}
}
/**
* Check if docs-v2 repository is accessible.
*/
async function validateDocsV2Path() {
try {
await fs.access('../..');
return true;
} catch (error) {
console.warn('⚠️ Warning: docs-v2 repository structure not detected');
console.warn(
' Make sure you are running this from docs-v2/helper-scripts/influxdb3-plugins'
);
return false;
}
}
/**
* Parse command line arguments.
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
config: 'docs_mapping.yaml',
plugin: null,
dryRun: false,
validate: false,
help: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--config':
options.config = args[++i];
break;
case '--plugin':
options.plugin = args[++i];
break;
case '--dry-run':
options.dryRun = true;
break;
case '--validate':
options.validate = true;
break;
case '--help':
case '-h':
options.help = true;
break;
default:
console.error(`Unknown argument: ${arg}`);
options.help = true;
break;
}
}
return options;
}
/**
* Show help message.
*/
function showHelp() {
console.log(`
Transform plugin READMEs from influxdb3_plugins to docs-v2 format
Usage: node port_to_docs.js [options]
Options:
--config <file> Path to mapping configuration file (default: docs_mapping.yaml)
--plugin <name> Process only specified plugin
--dry-run Show what would be done without making changes
--validate Validate configuration only
--help, -h Show this help message
Examples:
node port_to_docs.js # Process all plugins
node port_to_docs.js --plugin basic_transformation # Process specific plugin
node port_to_docs.js --dry-run # Preview changes
node port_to_docs.js --validate # Check configuration
`);
}
/**
* Main transformation function.
*/
async function main() {
const options = parseArgs();
if (options.help) {
showHelp();
process.exit(0);
}
// Load configuration
const config = await loadMappingConfig(options.config);
if (!config || !config.plugins) {
console.error('❌ Invalid configuration file');
process.exit(1);
}
// Validate configuration
if (options.validate) {
console.log('Validating configuration...');
let valid = true;
for (const [pluginName, mapping] of Object.entries(config.plugins)) {
if (!mapping.source || !mapping.target) {
console.error(
`❌ Invalid mapping for ${pluginName}: missing source or target`
);
valid = false;
continue;
}
try {
await fs.access(mapping.source);
} catch (error) {
console.warn(
`⚠️ Source not found for ${pluginName}: ${mapping.source}`
);
}
}
if (valid) {
console.log('✅ Configuration is valid');
}
process.exit(valid ? 0 : 1);
}
// Check if we're in the right location
if (!options.dryRun && !(await validateDocsV2Path())) {
console.log(
'\nTo use this script, ensure you are in the correct directory:'
);
console.log(' cd docs-v2/helper-scripts/influxdb3-plugins');
process.exit(1);
}
// Process plugins
let pluginsToProcess = Object.entries(config.plugins);
if (options.plugin) {
if (!config.plugins[options.plugin]) {
console.error(`❌ Plugin '${options.plugin}' not found in configuration`);
process.exit(1);
}
pluginsToProcess = [[options.plugin, config.plugins[options.plugin]]];
}
console.log(
`${options.dryRun ? 'DRY RUN: ' : ''}Processing ${pluginsToProcess.length} plugin(s)...\n`
);
let successCount = 0;
let errorCount = 0;
for (const [pluginName, mapping] of pluginsToProcess) {
if (await processPlugin(pluginName, mapping, options.dryRun)) {
successCount++;
} else {
errorCount++;
}
}
// Print summary
console.log('\n' + '='.repeat(60));
console.log('TRANSFORMATION SUMMARY');
console.log('='.repeat(60));
console.log(`Successfully processed: ${successCount}`);
console.log(`Errors: ${errorCount}`);
if (errorCount === 0) {
console.log('\n✅ All plugins processed successfully!');
if (!options.dryRun) {
console.log('\nNext steps:');
console.log('1. Review the generated documentation in docs-v2');
console.log('2. Test that all links work correctly');
console.log('3. Verify product shortcodes render properly');
console.log('4. Commit changes in both repositories');
}
} else {
console.log(`\n${errorCount} plugin(s) failed to process`);
process.exit(1);
}
}
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// Run main function
if (import.meta.url.endsWith(process.argv[1])) {
main().catch((error) => {
console.error('❌ Fatal error:', error.message);
process.exit(1);
});
}
export { transformContent, processPlugin, loadMappingConfig };