From c628b2ab683cdd937da0386e71fa19ed2d75fd67 Mon Sep 17 00:00:00 2001 From: Carlos Santos <4a.santos@gmail.com> Date: Wed, 11 Jun 2025 12:49:41 +0200 Subject: [PATCH] ov-components: enhance directive table generation logic and improve error handling --- .../doc/scripts/generate-directive-tables.js | 438 ++++++++++++------ 1 file changed, 302 insertions(+), 136 deletions(-) diff --git a/openvidu-components-angular/projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js b/openvidu-components-angular/projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js index db47bc84..f1d7eeb6 100644 --- a/openvidu-components-angular/projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js +++ b/openvidu-components-angular/projects/openvidu-components-angular/doc/scripts/generate-directive-tables.js @@ -3,168 +3,334 @@ const glob = require('glob'); const startApiLine = ''; const apiDirectivesTable = - '| **Parameter** | **Type** | **Reference** | \n' + - '|:--------------------------------: | :-------: | :---------------------------------------------: |'; + '| **Parameter** | **Type** | **Reference** | \n' + + '|:--------------------------------: | :-------: | :---------------------------------------------: |'; const endApiLine = ''; +/** + * Get all directive files from the API directives directory + */ function getDirectiveFiles() { - // Directory where directive files are located - const directivesDir = 'projects/openvidu-components-angular/src/lib/directives/api'; - return listFiles(directivesDir, '.directive.ts'); + const directivesDir = 'projects/openvidu-components-angular/src/lib/directives/api'; + return listFiles(directivesDir, '.directive.ts'); } +/** + * Get all component files + */ function getComponentFiles() { - // Directory where component files are located - const componentsDir = 'projects/openvidu-components-angular/src/lib/components'; - return listFiles(componentsDir, '.component.ts'); + const componentsDir = 'projects/openvidu-components-angular/src/lib/components'; + return listFiles(componentsDir, '.component.ts'); } +/** + * Get all admin files + */ function getAdminFiles() { - // Directory where component files are located - const componentsDir = 'projects/openvidu-components-angular/src/lib/admin'; - return listFiles(componentsDir, '.component.ts'); + const componentsDir = 'projects/openvidu-components-angular/src/lib/admin'; + return listFiles(componentsDir, '.component.ts'); } +/** + * List all files with specific extension in directory + */ function listFiles(directoryPath, fileExtension) { - const files = glob.sync(`${directoryPath}/**/*${fileExtension}`); - if (files.length === 0) { - throw new Error(`No ${fileExtension} files found in ${directoryPath}`); - } - return files; + const files = glob.sync(`${directoryPath}/**/*${fileExtension}`); + if (files.length === 0) { + throw new Error(`No ${fileExtension} files found in ${directoryPath}`); + } + return files; } +/** + * Extract component selector from component file + */ +function getComponentSelector(componentFile) { + const componentContent = fs.readFileSync(componentFile, 'utf8'); + const selectorMatch = componentContent.match(/@Component\({[^]*?selector:\s*['"]([^'"]+)['"][^]*?}\)/s); + + if (!selectorMatch) { + throw new Error(`Unable to find selector in component file: ${componentFile}`); + } + + return selectorMatch[1]; +} + +/** + * Check if a directive class has @internal annotation + */ +function isInternalDirective(directiveContent, className) { + const classRegex = new RegExp(`(/\\*\\*[\\s\\S]*?\\*/)?\\s*@Directive\\([\\s\\S]*?\\)\\s*export\\s+class\\s+${escapeRegex(className)}`, 'g'); + const match = classRegex.exec(directiveContent); + + if (match && match[1]) { + return match[1].includes('@internal'); + } + + return false; +} + +/** + * Extract attribute name from selector for a specific component + */ +function extractAttributeForComponent(selector, componentSelector) { + // Split selector by comma and trim whitespace + const selectorParts = selector.split(',').map(part => part.trim()); + + // Find the part that matches our component + for (const part of selectorParts) { + if (part.includes(componentSelector)) { + // Extract attribute from this specific part + const attributeMatch = part.match(/\[([^\]]+)\]/); + if (attributeMatch) { + return attributeMatch[1]; + } + } + } + + // Fallback: if no specific match, return the first attribute found + const fallbackMatch = selector.match(/\[([^\]]+)\]/); + return fallbackMatch ? fallbackMatch[1] : null; +} + +/** + * Extract all directive classes from a directive file + */ +function extractDirectiveClasses(directiveContent) { + const classes = []; + + // Regex to find all directive class definitions with their preceding @Directive decorators + const directiveClassRegex = /@Directive\(\s*{\s*selector:\s*['"]([^'"]+)['"][^}]*}\s*\)\s*export\s+class\s+(\w+)/gs; + + let match; + while ((match = directiveClassRegex.exec(directiveContent)) !== null) { + const selector = match[1]; + const className = match[2]; + + // Skip internal directives + if (isInternalDirective(directiveContent, className)) { + console.log(`Skipping internal directive: ${className}`); + continue; + } + + classes.push({ + selector, + className + }); + } + + return classes; +} + +/** + * Extract all directives from a directive file that match a component selector + */ +function extractDirectivesForComponent(directiveFile, componentSelector) { + const directiveContent = fs.readFileSync(directiveFile, 'utf8'); + const directives = []; + + // Get all directive classes in the file (excluding internal ones) + const directiveClasses = extractDirectiveClasses(directiveContent); + + // Filter classes that match the component selector + const matchingClasses = directiveClasses.filter(directiveClass => + directiveClass.selector.includes(componentSelector) + ); + + // For each matching class, extract input type information + matchingClasses.forEach(directiveClass => { + // Extract the correct attribute name for this component + const attributeName = extractAttributeForComponent(directiveClass.selector, componentSelector); + + if (attributeName) { + const inputInfo = extractInputInfo(directiveContent, attributeName, directiveClass.className); + + if (inputInfo) { + directives.push({ + attribute: attributeName, + type: inputInfo.type, + className: directiveClass.className + }); + } + } + }); + + return directives; +} + +/** + * Extract input information (type) for a specific attribute and class + */ +function extractInputInfo(directiveContent, attributeName, className) { + // Create a regex to find the specific class section + const classRegex = new RegExp(`export\\s+class\\s+${escapeRegex(className)}[^}]*?{([^]*?)(?=export\\s+class|$)`, 's'); + const classMatch = directiveContent.match(classRegex); + + if (!classMatch) { + console.warn(`Could not find class ${className}`); + return null; + } + + const classContent = classMatch[1]; + + // Regex to find the @Input setter for this attribute within the class + const inputRegex = new RegExp( + `@Input\\(\\)\\s+set\\s+${escapeRegex(attributeName)}\\s*\\(\\s*\\w+:\\s*([^)]+)\\s*\\)`, + 'g' + ); + + const inputMatch = inputRegex.exec(classContent); + if (!inputMatch) { + console.warn(`Could not find @Input setter for attribute: ${attributeName} in class: ${className}`); + return null; + } + + let type = inputMatch[1].trim(); + + // Clean up the type (remove extra whitespace, etc.) + type = type.replace(/\s+/g, ' '); + + return { + type: type + }; +} + +/** + * Escape special regex characters + */ +function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +/** + * Generate API directives table for components + */ +function generateApiDirectivesTable(componentFiles, directiveFiles) { + componentFiles.forEach((componentFile) => { + try { + console.log(`Processing component: ${componentFile}`); + + const componentSelector = getComponentSelector(componentFile); + const readmeFilePath = componentFile.replace('.ts', '.md'); + + console.log(`Component selector: ${componentSelector}`); + + // Initialize table with header + initializeDynamicTableContent(readmeFilePath); + + const allDirectives = []; + + // Extract directives from all directive files + directiveFiles.forEach((directiveFile) => { + console.log(`Checking directive file: ${directiveFile}`); + const directives = extractDirectivesForComponent(directiveFile, componentSelector); + allDirectives.push(...directives); + }); + + console.log(`Found ${allDirectives.length} directives for ${componentSelector}`); + + // Sort directives alphabetically by attribute name + allDirectives.sort((a, b) => a.attribute.localeCompare(b.attribute)); + + // Add rows to table + allDirectives.forEach((directive) => { + addRowToTable(readmeFilePath, directive.attribute, directive.type, directive.className); + }); + + // If no directives found, add "no directives" message + if (allDirectives.length === 0) { + removeApiTableContent(readmeFilePath); + } + + } catch (error) { + console.error(`Error processing component ${componentFile}:`, error.message); + } + }); +} + +/** + * Initialize table with header + */ function initializeDynamicTableContent(filePath) { - replaceDynamicTableContent(filePath, apiDirectivesTable); + replaceDynamicTableContent(filePath, apiDirectivesTable); } +/** + * Replace table content with "no directives" message + */ function removeApiTableContent(filePath) { - const content = '_No API directives available for this component_. \n'; - replaceDynamicTableContent(filePath, content); + const content = '_No API directives available for this component_. \n'; + replaceDynamicTableContent(filePath, content); } -function apiTableContentIsEmpty(filePath) { - try { - const data = fs.readFileSync(filePath, 'utf8'); - const startIdx = data.indexOf(startApiLine); - const endIdx = data.indexOf(endApiLine); - if (startIdx !== -1 && endIdx !== -1) { - const capturedContent = data.substring(startIdx + startApiLine.length, endIdx).trim(); - return capturedContent === apiDirectivesTable; - } - return false; - } catch (error) { - return false; - } -} - -function writeApiDirectivesTable(componentFiles, directiveFiles) { - componentFiles.forEach((componentFile) => { - // const componentName = componentFile.split('/').pop() - const componentFileName = componentFile.split('/').pop().replace('.component.ts', ''); - const componentName = componentFileName.replace(/(?:^|-)([a-z])/g, (_, char) => char.toUpperCase()); - const readmeFilePath = componentFile.replace('.ts', '.md'); - const componentContent = fs.readFileSync(componentFile, 'utf8'); - const selectorMatch = componentContent.match(/@Component\({[^]*?selector: ['"]([^'"]+)['"][^]*?}\)/); - const componentSelectorName = selectorMatch[1]; - initializeDynamicTableContent(readmeFilePath); - - if (!componentSelectorName) { - throw new Error(`Unable to find the component name in the file ${componentFileName}`); - } - - // const directiveRegex = new RegExp(`@Directive\\(\\s*{[^}]*selector:\\s*['"]${componentName}\\s*\\[([^'"]+)\\]`, 'g'); - const directiveRegex = /^\s*(selector):\s*(['"])(.*?)\2\s*$/gm; - - directiveFiles.forEach((directiveFile) => { - const directiveContent = fs.readFileSync(directiveFile, 'utf8'); - - let directiveNameMatch; - while ((directiveNameMatch = directiveRegex.exec(directiveContent)) !== null) { - if (directiveNameMatch[0].includes('@Directive({\n//')) { - // Skip directives that are commented out - continue; - } - const selectorValue = directiveNameMatch[3].split(','); - const directiveMatch = selectorValue.find((value) => value.includes(componentSelectorName)); - - if (directiveMatch) { - const directiveName = directiveMatch.match(/\[(.*?)\]/).pop(); - const className = directiveName.replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase()) + 'Directive'; - const inputRegex = new RegExp( - `@Input\\(\\)\\s+set\\s+(${directiveName.replace(/\[/g, '\\[').replace(/\]/g, '\\]')})\\((\\w+):\\s+(\\w+)` - ); - const inputMatch = directiveContent.match(inputRegex); - const inputType = inputMatch && inputMatch.pop(); - - if (inputType && className) { - let finalClassName = componentName === 'Videoconference' ? className : componentName + className; - addRowToTable(readmeFilePath, directiveName, inputType, finalClassName); - } - } else { - console.log(`The selector "${componentSelectorName}" does not match with ${selectorValue}. Skipping...`); - } - } - }); - - if (apiTableContentIsEmpty(readmeFilePath)) { - removeApiTableContent(readmeFilePath); - } - }); -} - -// Function to add a row to a Markdown table in a file +/** + * Add a row to the markdown table + */ function addRowToTable(filePath, parameter, type, reference) { - // Read the current content of the file - try { - const data = fs.readFileSync(filePath, 'utf8'); + try { + const data = fs.readFileSync(filePath, 'utf8'); + const markdownRow = `| **${parameter}** | \`${type}\` | [${reference}](../directives/${reference}.html) |`; - // Define the target line and the Markdown row - const markdownRow = `| **${parameter}** | \`${type}\` | [${reference}](../directives/${reference}.html) |`; + const lines = data.split('\n'); + const targetIndex = lines.findIndex((line) => line.includes(endApiLine)); - // Find the line that contains the table - const lines = data.split('\n'); - const targetIndex = lines.findIndex((line) => line.includes(endApiLine)); - - if (targetIndex !== -1) { - // Insert the new row above the target line - lines.splice(targetIndex, 0, markdownRow); - - // Join the lines back together - const updatedContent = lines.join('\n'); - - // Write the updated content to the file - fs.writeFileSync(filePath, updatedContent, 'utf8'); - console.log('Row added successfully.'); - } else { - console.error('Table not found in the file.'); - } - } catch (error) { - console.error('Error writing to file:', error); - } + if (targetIndex !== -1) { + lines.splice(targetIndex, 0, markdownRow); + const updatedContent = lines.join('\n'); + fs.writeFileSync(filePath, updatedContent, 'utf8'); + console.log(`Added directive: ${parameter} -> ${reference}`); + } else { + console.error('End marker not found in file:', filePath); + } + } catch (error) { + console.error('Error adding row to table:', error); + } } +/** + * Replace content between start and end markers + */ function replaceDynamicTableContent(filePath, content) { - // Read the current content of the file - try { - const data = fs.readFileSync(filePath, 'utf8'); - const pattern = new RegExp(`${startApiLine}([\\s\\S]*?)${endApiLine}`, 'g'); + try { + const data = fs.readFileSync(filePath, 'utf8'); + const pattern = new RegExp(`${startApiLine}([\\s\\S]*?)${endApiLine}`, 'g'); - // Replace the content between startLine and endLine with the replacement table - const modifiedContent = data.replace(pattern, (match, capturedContent) => { - return startApiLine + '\n' + content + '\n' + endApiLine; - }); - // Write the modified content back to the file - fs.writeFileSync(filePath, modifiedContent, 'utf8'); - } catch (error) { - if (error.code === 'ENOENT') { - console.log(`${filePath} not found! Maybe it is an internal component. Skipping...`); - } else { - console.error('Error writing to file:', error); - } - } + const modifiedContent = data.replace(pattern, (match, capturedContent) => { + return startApiLine + '\n' + content + '\n' + endApiLine; + }); + + fs.writeFileSync(filePath, modifiedContent, 'utf8'); + console.log(`Updated table content in: ${filePath}`); + } catch (error) { + if (error.code === 'ENOENT') { + console.log(`${filePath} not found! Maybe it is an internal component. Skipping...`); + } else { + console.error('Error writing to file:', error); + } + } } -const directiveFiles = getDirectiveFiles(); -const componentFiles = getComponentFiles(); -const adminFiles = getAdminFiles(); -writeApiDirectivesTable(componentFiles.concat(adminFiles), directiveFiles); +// Main execution +if (require.main === module) { + try { + const directiveFiles = getDirectiveFiles(); + const componentFiles = getComponentFiles(); + const adminFiles = getAdminFiles(); + + console.log('Starting directive table generation...'); + generateApiDirectivesTable(componentFiles.concat(adminFiles), directiveFiles); + console.log('Directive table generation completed!'); + + } catch (error) { + console.error('Script execution failed:', error); + process.exit(1); + } +} + +// Export functions for testing +module.exports = { + generateApiDirectivesTable, + getDirectiveFiles, + getComponentFiles, + getAdminFiles +}; \ No newline at end of file