Created
March 25, 2025 20:45
-
-
Save eneajaho/8f3971d46bca7345a4bb48ceb0bf299a to your computer and use it in GitHub Desktop.
Check components on project if they are OnPush
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env node | |
const fs = require('fs'); | |
const path = require('path'); | |
const ts = require('typescript'); // Requires 'npm install typescript' | |
// --- Configuration --- | |
const DEFAULT_PROJECT_PATH = '.'; // Default to current directory | |
const EXCLUDED_DIRS = ['node_modules', 'dist', '.angular', '.vscode', '.git']; // Directories to skip | |
const COMPONENT_FILE_SUFFIX = '.component.ts'; | |
// --- End Configuration --- | |
function findAngularComponents(dir, allComponents, nonOnPushComponents) { | |
try { | |
const entries = fs.readdirSync(dir, { withFileTypes: true }); | |
for (const entry of entries) { | |
const fullPath = path.join(dir, entry.name); | |
if (entry.isDirectory()) { | |
// Skip excluded directories | |
if (!EXCLUDED_DIRS.includes(entry.name)) { | |
findAngularComponents(fullPath, allComponents, nonOnPushComponents); | |
} | |
} else if (entry.isFile() && entry.name.endsWith(COMPONENT_FILE_SUFFIX)) { | |
analyzeComponentFile(fullPath, allComponents, nonOnPushComponents); | |
} | |
} | |
} catch (err) { | |
console.error(`Error reading directory ${dir}:`, err.message); | |
// Decide if you want to stop execution or just skip this directory | |
// process.exit(1); | |
} | |
} | |
function analyzeComponentFile(filePath, allComponents, nonOnPushComponents) { | |
try { | |
const fileContent = fs.readFileSync(filePath, 'utf8'); | |
const sourceFile = ts.createSourceFile( | |
filePath, | |
fileContent, | |
ts.ScriptTarget.Latest, // Use appropriate TS version target if needed | |
true // setParentNodes flag | |
); | |
let isComponent = false; | |
let usesOnPush = false; | |
ts.forEachChild(sourceFile, visitNode); | |
function visitNode(node) { | |
// Check if it's a class declaration | |
if (ts.isClassDeclaration(node)) { | |
const decorators = ts.getDecorators ? ts.getDecorators(node) : node.decorators; // Check for TS version compatibility | |
if (decorators) { | |
for (const decorator of decorators) { | |
// Check if the decorator is @Component | |
if (ts.isCallExpression(decorator.expression)) { | |
const decoratorName = decorator.expression.expression.getText(sourceFile); | |
if (decoratorName === 'Component') { | |
isComponent = true; | |
allComponents.push(filePath); // Add to total component list | |
// Get the argument object of the decorator | |
const arg = decorator.expression.arguments[0]; | |
if (arg && ts.isObjectLiteralExpression(arg)) { | |
// Find the changeDetection property | |
for (const prop of arg.properties) { | |
if (ts.isPropertyAssignment(prop) && prop.name.getText(sourceFile) === 'changeDetection') { | |
// Check if the value is ChangeDetectionStrategy.OnPush | |
if (prop.initializer && ts.isPropertyAccessExpression(prop.initializer)) { | |
const strategyText = prop.initializer.getText(sourceFile); | |
if (strategyText === 'ChangeDetectionStrategy.OnPush') { | |
usesOnPush = true; | |
} | |
} | |
// Found the changeDetection property, no need to check further properties | |
break; | |
} | |
} | |
} | |
// Found the @Component decorator, no need to check other decorators on this class | |
break; | |
} | |
} | |
} | |
} | |
} | |
// Only continue traversal if we haven't confirmed it's an OnPush component | |
if (!isComponent || !usesOnPush) { | |
ts.forEachChild(node, visitNode); | |
} | |
} | |
// After checking the file, if it's a component but not OnPush, add it to the list | |
if (isComponent && !usesOnPush) { | |
nonOnPushComponents.push(filePath); | |
} | |
} catch (err) { | |
console.error(`Error analyzing file ${filePath}:`, err.message); | |
} | |
} | |
// --- Main Execution --- | |
const projectPath = path.resolve(process.argv[2] || DEFAULT_PROJECT_PATH); | |
console.log(`Analyzing Angular components in: ${projectPath}\n`); | |
if (!fs.existsSync(projectPath) || !fs.statSync(projectPath).isDirectory()) { | |
console.error(`Error: Project path "${projectPath}" does not exist or is not a directory.`); | |
process.exit(1); | |
} | |
const allComponentsList = []; | |
const nonOnPushComponentsList = []; | |
findAngularComponents(projectPath, allComponentsList, nonOnPushComponentsList); | |
// --- Report Results --- | |
console.log('--- Analysis Summary ---'); | |
console.log(`Total Components Found: ${allComponentsList.length}`); | |
console.log(`Components NOT using OnPush: ${nonOnPushComponentsList.length}`); | |
console.log('------------------------\n'); | |
if (nonOnPushComponentsList.length > 0) { | |
console.log('Components potentially needing ChangeDetectionStrategy.OnPush:'); | |
nonOnPushComponentsList.forEach(filePath => { | |
// Make path relative to the initial project path for cleaner output | |
console.log(` - ${path.relative(projectPath, filePath)}`); | |
}); | |
} else if (allComponentsList.length > 0) { | |
console.log('All identified components are using OnPush or change detection was not explicitly set (default).'); | |
console.log('Note: This script checks for explicit "ChangeDetectionStrategy.OnPush". Components without an explicit strategy use the default (CheckAlways).') | |
} else { | |
console.log('No Angular component files (*.component.ts) found in the specified directory.'); | |
} | |
console.log('\nAnalysis complete.'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Added some changes with Gemini as well to pass in the maximum components allowed and make it fail on the CI. To make sure that the amount does not increase and help transition things better.
Thanks for the script!