Count Lines of Code in a Monorepo (Exclude node_modules)
The Illusion of Scale: 5,240,000 Lines vs. 85,432 Lines of Actual Code
Picture this scenario: You run a quick line-counting command on your company’s flagship GitHub monorepo. The terminal spits out a staggering 5,240,000 lines of code. You feel a surge of pride, assuming your engineering team is building a massive, complex empire. But then, a senior staff engineer runs a properly configured Source Lines of Code (SLOC) analyzer. The real number? Just 85,432 lines. The remaining 5.15 million lines are simply hoisted dependencies, lockfiles, minified build artifacts, and machine-generated types. This stark contrast highlights a critical problem in modern software metrics. Counting total lines in a repository is trivial; learning how to accurately count source lines of code in a GitHub monorepo excluding node_modules and auto-generated files is an entirely different engineering challenge.
The Terminal Trap: Naive Shell Commands vs. Purpose-Built SLOC Engines
When developers first attempt to measure a monorepo, they usually reach for the most basic tools available in their terminal. The contrast between these rudimentary methods and modern analysis engines is the difference between guessing and knowing.
The Naive Approach: Blind Shell Piping
The most common mistake is using standard Unix utilities. A developer might run a command like find . -type f -name "*.ts" | xargs wc -l. This approach is fundamentally flawed for a monorepo. It blindly traverses every directory, meaning it will happily count every single TypeScript file buried deep inside your node_modules folder. It does not understand context, it does not respect version control boundaries, and it will inevitably crash or hang when it hits hundreds of thousands of nested dependency files.
The Professional Approach: High-Performance SLOC Tools
To accurately count source lines of code, you must use purpose-built tools written in compiled languages like Rust or Go. Tools such as scc (Sloc Cloc and Code) or tokei are designed specifically for this task. They are blazingly fast, capable of scanning millions of files in seconds. More importantly, they possess built-in language detection and, crucially, they automatically respect your .gitignore files. By simply running scc . in your root directory, you instantly bypass the noise of untracked build artifacts and dependencies.
The Configuration Battle: Hardcoded Ignore Lists vs. Centralized Filtering
Even with the right tool, a monorepo’s complex structure requires careful configuration. How you manage your exclusion rules determines whether your metrics remain accurate over time.
The Naive Approach: Endless CLI Flags
Many teams try to solve the exclusion problem by passing massive strings of command-line arguments. You end up with unwieldy commands like cloc --exclude-dir=node_modules,dist,build,coverage,.next,.turbo,.cache .. This is a maintenance nightmare. Every time a new framework is added to the monorepo—introducing a new build directory like .vite or .svelte-kit—someone has to remember to update the shell script. Inevitably, they forget, and your SLOC metrics become corrupted again.
The Professional Approach: Git-Aware and Centralized Configurations
The superior method relies on centralized configuration files that travel with your repository. If you use cloc, you can create a .clocignore file in the root of your monorepo. However, the absolute best practice is leveraging tools that natively parse your existing .gitignore. Since a well-maintained GitHub monorepo already excludes node_modules, dist, and environment files from version control, a git-aware SLOC tool will automatically ignore them without any extra configuration. For workspace-specific artifacts, adding a simple .gitignore in the specific package directory keeps your root metrics clean.
The Auto-Generated Menace: Blind Inclusion vs. Semantic Exclusions
The most insidious inflator of code metrics in modern web development is not third-party dependencies, but the code your own tools write for you. Differentiating between human-authored logic and machine-generated boilerplate is essential.
The Naive Approach: Counting Everything in the Source Tree
Consider a standard Next.js monorepo utilizing Prisma and GraphQL. A single schema.prisma file of 200 lines can generate a node_modules/.prisma/client/index.d.ts file exceeding 15,000 lines of TypeScript definitions. If you have three micro-frontends in your workspace, that is 45,000 lines of machine-written code instantly inflating your repository metrics. If your team counts these auto-generated files, a developer who adds one database table appears to have written 2,000 lines of code in a single commit. This completely destroys the validity of your engineering analytics.
The Professional Approach: Regex and Directory-Level Semantic Filtering
To fix this, you must explicitly target auto-generated files. Most modern codegen tools output to predictable locations. You should configure your SLOC tool to ignore directories named __generated__, .gen, or generated. If you are using scc, you can utilize the --exclude-file flag with a regular expression to skip files ending in .generated.ts or .pb.go. By stripping away Prisma clients, GraphQL resolvers, and Protobuf stubs, you reveal the true footprint of your engineering team's actual intellectual property.
Workflow Integration: Ad-Hoc Local Checks vs. Automated CI/CD Pipelines
Finally, the way your team consumes these metrics dictates their long-term value. Relying on human memory to run these checks guarantees eventual failure.
The Naive Approach: Manual Local Execution
In many organizations, counting lines of code is an ad-hoc activity. A tech lead remembers to run a script locally once a quarter, copies the output, and pastes it into a spreadsheet. This approach is highly prone to human error. The developer might forget to pull the latest main branch, or they might run the command from a sub-directory rather than the monorepo root, resulting in fragmented and inaccurate reporting.
The Professional Approach: Automated GitHub Actions Metrics
The ultimate solution is to remove the human element entirely by integrating SLOC analysis directly into your CI/CD pipeline. You can create a GitHub Actions workflow that triggers on every pull request to the main branch. This workflow checks out the code, runs a tool like scc with your strict exclusion rules, and uses a GitHub Action to post a comment on the PR detailing the exact delta in source lines of code. This provides immediate, accurate feedback to reviewers. It transforms SLOC from a vague, quarterly vanity metric into a precise, real-time engineering signal, ensuring that your monorepo's growth is always measured by the code your team actually writes.
Frequently Asked Questions
How do I count lines of code in a GitHub repository?
You can count lines of code in a GitHub repository by cloning the repo locally and using command-line tools like `cloc` or `tokei`. These tools analyze your codebase and provide a breakdown of lines, comments, and blank spaces by programming language.
How do I exclude node_modules when counting lines of code?
When using tools like `cloc` or `scc`, you can exclude the `node_modules` directory by passing a specific flag like `--exclude-dir=node_modules`. Alternatively, you can configure the tool to respect your `.gitignore` file, which automatically skips ignored dependencies.
What is the best tool to count lines of code in a monorepo?
`Tokei` and `cloc` are widely considered the best tools for counting lines of code in a monorepo due to their speed and configuration options. `Tokei` is written in Rust and is exceptionally fast, while `cloc` offers highly granular exclusion rules for complex directory structures. Both handle large monorepos efficiently.
How do I ignore auto-generated files when counting LOC?
To ignore auto-generated files, you can use the `--exclude-list` flag in `cloc` to specify a text file containing patterns or filenames to skip. Tools like `tokei` also allow you to configure a `.tokeiignore` file, functioning similarly to `.gitignore` to filter out build artifacts and generated code.
Can I count lines of code directly using the GitHub API?
The GitHub API does not provide a native endpoint for counting lines of code, but you can use the Linguist library which powers GitHub's language stats. To get accurate counts, it is better to clone the repository and run a local counting tool. This approach gives you full control over exclusions and generated file filtering.
How do I count lines of code per package in a monorepo?
To count lines of code per package in a monorepo, you can run your LOC tool targeting specific subdirectories like `packages/*` or `apps/*`. For example, using `cloc packages/*` will generate an individual report for each package directory. This helps developers track code size and complexity across different microservices or libraries.
Does `cloc` respect `.gitignore` automatically?
By default, `cloc` does not automatically respect `.gitignore`, but you can force it to do so using the `--vcs` flag. Running `cloc --vcs git` will instruct the tool to only count files tracked by Git, effectively ignoring `node_modules` and other untracked files. This is the most efficient way to filter out unwanted directories.
How do I count lines of code for a specific language only?
Most LOC tools allow you to filter by language using specific flags, such as `cloc --include-lang=TypeScript` or `tokei -t typescript`. This is particularly useful in a monorepo where you want to measure the size of backend code separately from frontend code. It prevents auto-generated configuration files from skewing your primary language metrics.
How do I exclude test files from my lines of code count?
You can exclude test files by using regex patterns or directory exclusions in your LOC tool, such as `--exclude-dir=tests` or `--not-match-f='_test.go$'`. Another approach is to use a custom ignore file to list all test file extensions and naming conventions. This ensures your count reflects only the production source code.