Skip to content

Instantly share code, notes, and snippets.

@eneajaho
Created March 25, 2025 20:45
Show Gist options
  • Save eneajaho/8f3971d46bca7345a4bb48ceb0bf299a to your computer and use it in GitHub Desktop.
Save eneajaho/8f3971d46bca7345a4bb48ceb0bf299a to your computer and use it in GitHub Desktop.
Check components on project if they are OnPush
#!/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.');
@gabynevada
Copy link

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment