Developers spend a surprising amount of time on repetitive tasks: checking project files, validating configurations, finding code issues, and debugging unexpected behavior. Small developer tools can automate these tasks, reduce mistakes, and make everyday workflows significantly faster.
JavaScript is an excellent language for building developer tools because Node.js provides direct access to the filesystem, process arguments, environment variables, and command-line output. You can build everything from simple code generators to sophisticated command-line interfaces without introducing another programming language.
In this tutorial, we will build a practical command-line code analyzer using Node.js. It will scan JavaScript files, count lines and comments, detect common coding patterns, generate a readable report, and support command-line options so you can adapt it to different projects.
Building a JavaScript Code Analyzer with Node.js
A useful developer tool should solve one specific problem reliably, provide actionable output, and fit naturally into an existing workflow. Our code analyzer will accept a directory path, recursively discover JavaScript files, inspect their contents, and display statistics alongside potential code-quality warnings. This makes it useful as a starting point for a local linting utility or a lightweight project-inspection tool.
Node.js provides the built-in fs and path modules for filesystem operations, while the process object exposes command-line arguments and exit codes. We will use these APIs to avoid external dependencies and keep the example easy to run. The implementation also separates file discovery, source analysis, reporting, and argument parsing into individual functions, making future extensions easier to maintain.
The analyzer checks for several common patterns: console statements, debugging statements, TODO comments, and potentially oversized files. These checks are intentionally simple and are not substitutes for a proper JavaScript parser or production linting rules. In real projects, you could extend the tool with AST-based analysis, configurable rules, JSON output, Git integration, or continuous integration checks.
Save the following source as dev-tool.js. Run node dev-tool.js ./src to analyze a source directory, or use node dev-tool.js ./src --json to produce machine-readable output. The script also supports --help and returns a nonzero exit code when it detects configured warnings, making it suitable for automation pipelines.
const fs = require('node:fs');
const path = require('node:path');
// Define the command-line options supported by this tool.
const args = process.argv.slice(2);
const showHelp = args.includes('--help') || args.includes('-h');
const jsonOutput = args.includes('--json');
const targetArg = args.find((arg) => !arg.startsWith('--') && !arg.startsWith('-'));
const targetDir = path.resolve(targetArg || '.');
// Keep the rules in one place so they can be configured later.
const MAX_LINES = 300;
const SOURCE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']);
function printHelp() {
console.log('\nJavaScript Developer Tool: Code Analyzer\n');
console.log('Usage: node dev-tool.js <directory> [options]');
console.log('\nOptions:');
console.log(' --json Print the report as JSON');
console.log(' --help Display this help message');
console.log('\nExample: node dev-tool.js ./src --json\n');
}
function findJavaScriptFiles(directory, results = []) {
// Read directory entries with their file types to avoid unnecessary stats.
const entries = fs.readdirSync(directory, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
// Ignore common generated and dependency directories.
if (entry.isDirectory()) {
if (['node_modules', '.git', 'dist', 'build'].includes(entry.name)) {
continue;
}
findJavaScriptFiles(fullPath, results);
continue;
}
// Include only JavaScript source files.
if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
results.push(fullPath);
}
}
return results;
}
function analyzeFile(filePath) {
const source = fs.readFileSync(filePath, 'utf8');
const lines = source.split(/\r?\n/);
// These simple patterns are useful signals, not full syntax validation.
const consoleMatches = source.match(/\bconsole\.(log|warn|error|debug)\s*\(/g) || [];
const debuggerMatches = source.match(/\bdebugger\s*;?/g) || [];
const todoMatches = source.match(/\bTODO\b/gi) || [];
const commentLines = lines.filter((line) => /^\s*(\/\/|\/\*|\*|\*\/)/.test(line));
const blankLines = lines.filter((line) => line.trim() === '');
// Collect warnings separately so reporting stays independent of analysis.
const warnings = [];
if (lines.length > MAX_LINES) {
warnings.push(`File exceeds ${MAX_LINES} lines`);
}
if (debuggerMatches.length > 0) {
warnings.push(`Found ${debuggerMatches.length} debugger statement(s)`);
}
if (todoMatches.length > 0) {
warnings.push(`Found ${todoMatches.length} TODO comment(s)`);
}
return {
file: path.relative(process.cwd(), filePath),
lines: lines.length,
blankLines: blankLines.length,
commentLines: commentLines.length,
consoleStatements: consoleMatches.length,
debuggerStatements: debuggerMatches.length,
todos: todoMatches.length,
warnings,
};
}
function createReport(filePaths) {
const files = [];
console.log('[1/3] Discovering JavaScript files...');
console.log(`[2/3] Analyzing ${filePaths.length} file(s)...`);
for (const filePath of filePaths) {
try {
files.push(analyzeFile(filePath));
} catch (error) {
// Report individual failures without silently losing information.
files.push({
file: path.relative(process.cwd(), filePath),
error: error.message,
warnings: ['Unable to analyze file'],
});
}
}
// Aggregate statistics across successfully analyzed files.
const analyzedFiles = files.filter((file) => !file.error);
const totals = analyzedFiles.reduce((result, file) => {
result.lines += file.lines;
result.consoleStatements += file.consoleStatements;
result.debuggerStatements += file.debuggerStatements;
result.todos += file.todos;
result.warnings += file.warnings.length;
return result;
}, {
lines: 0,
consoleStatements: 0,
debuggerStatements: 0,
todos: 0,
warnings: 0,
});
return {
generatedAt: new Date().toISOString(),
directory: targetDir,
filesScanned: analyzedFiles.length,
filesFailed: files.length - analyzedFiles.length,
totals,
files,
};
}
function printReport(report) {
console.log('[3/3] Preparing report...\n');
if (jsonOutput) {
// JSON output makes the tool easy to consume in other scripts.
console.log(JSON.stringify(report, null, 2));
return;
}
console.log('=== JavaScript Code Analysis Report ===');
console.log(`Directory: ${report.directory}`);
console.log(`Files scanned: ${report.filesScanned}`);
console.log(`Files failed: ${report.filesFailed}`);
console.log(`Total lines: ${report.totals.lines}`);
console.log(`Console statements: ${report.totals.consoleStatements}`);
console.log(`Debugger statements: ${report.totals.debuggerStatements}`);
console.log(`TODO markers: ${report.totals.todos}`);
console.log(`Warnings: ${report.totals.warnings}\n`);
for (const file of report.files) {
console.log(`File: ${file.file}`);
if (file.error) {
console.log(` Error: ${file.error}`);
continue;
}
console.log(` Lines: ${file.lines}`);
console.log(` Blank lines: ${file.blankLines}`);
console.log(` Comment lines: ${file.commentLines}`);
console.log(` Console statements: ${file.consoleStatements}`);
console.log(` TODO markers: ${file.todos}`);
console.log(` Warnings: ${file.warnings.length ? file.warnings.join('; ') : 'None'}`);
}
}
function main() {
if (showHelp) {
printHelp();
return;
}
// Validate input before attempting to read the target directory.
if (!fs.existsSync(targetDir) || !fs.statSync(targetDir).isDirectory()) {
console.error(`Error: '${targetDir}' is not an existing directory.`);
process.exitCode = 2;
return;
}
console.log(`Starting analysis for: ${targetDir}\n`);
try {
const filePaths = findJavaScriptFiles(targetDir);
const report = createReport(filePaths);
printReport(report);
// A warning exit code lets CI pipelines detect problems automatically.
if (report.totals.warnings > 0 || report.filesFailed > 0) {
process.exitCode = 1;
}
} catch (error) {
console.error(`Analysis failed: ${error.message}`);
process.exitCode = 2;
}
}
// Run only when the file is executed directly, not when imported as a module.
if (require.main === module) {
main();
}
// Export reusable functions so the analyzer can be tested or extended.
module.exports = { findJavaScriptFiles, analyzeFile, createReport };
Conclusion
Creating developer tools with JavaScript is a practical way to improve productivity while strengthening your understanding of Node.js APIs. This example demonstrates how filesystem traversal, source inspection, reusable functions, command-line arguments, and exit codes come together to solve a real development problem.
The next step is to make the analyzer more reliable and configurable. You could add command-line flags for individual rules, exclude patterns from a configuration file, generate Markdown reports, or replace regular expressions with an Abstract Syntax Tree parser such as Acorn or Babel for more accurate analysis.
The most valuable developer tools often start small and evolve around actual workflow problems. Choose one repetitive task in your development process, automate it with a focused Node.js script, and gradually turn that script into a reusable CLI package when other developers can benefit from it.
Top comments (0)