Support component-based and Packwerk applications (#23)#47
Conversation
Add first-class support for large Rails monoliths organized into Packwerk packs (packs-rails) or components (component-based Rails). When packs or components are detected, `rake stats` now prints a breakdown per pack, a `Core Application` section, and a `Total (all packs)` section. The same breakdown is exposed in JSON under a `"packs"` key. Packs are detected by `PackFinder`: a directory is a pack when it holds a `package.yml` (Packwerk marker) or lives directly under `packs/`/`components/` with an `app`/`lib`/`spec`/`test` folder. Nested packs are supported. Also fixes the bug from #23 where an explicit path argument (`rake stats[packs/pack1]`) was ignored when running inside a booted Rails app because it was overwritten by `Rails.root`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #47 +/- ##
==========================================
+ Coverage 83.20% 86.25% +3.05%
==========================================
Files 19 23 +4
Lines 750 946 +196
==========================================
+ Hits 624 816 +192
- Misses 126 130 +4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds first-class support for Packwerk/packs-rails and component-based Rails monolith layouts by detecting packs/components and reporting rake stats output (console + JSON) broken down by “Core” plus per-pack groups, while keeping plain Rails apps’ output unchanged.
Changes:
- Introduces
RailsStats::PackFinderand adds grouped aggregation viaStatsCalculator#statistics_by_groupwith pack attribution. - Updates console and JSON formatters to render per-pack sections and a grand total when packs/components are present.
- Fixes
rake stats[packs/pack1]inside a booted Rails app by only defaulting toRails.rootwhen no path argument is provided; adds fixtures + tests + changelog/docs updates.
Reviewed changes
Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/rails_stats/pack_finder.rb | New pack/component detection used to enable grouped stats output. |
| lib/rails_stats/stats_calculator.rb | Adds pack detection, grouping logic, and per-group total helpers. |
| lib/rails_stats/console_formatter.rb | Prints per-pack sections + core + grand total when packs are detected. |
| lib/rails_stats/json_formatter.rb | Adds a "packs" breakdown section in JSON output when packs are detected. |
| lib/rails_stats/stats_formatter.rb | Plumbs statistics_by_group into formatters. |
| lib/rails_stats/tasks.rb | Fixes path argument handling in booted Rails apps for rake stats[PATH]. |
| lib/rails_stats/all.rb | Loads the new pack_finder implementation. |
| lib/rails_stats/app_statistics.rb | Exposes directory for grouping attribution. |
| lib/rails_stats/root_statistics.rb | Exposes directory for grouping attribution. |
| lib/rails_stats/spec_statistics.rb | Exposes directory for grouping attribution. |
| lib/rails_stats/test_statistics.rb | Exposes directory for grouping attribution. |
| lib/rails_stats/cucumber_statistics.rb | Exposes directory for grouping attribution. |
| lib/rails_stats/gem_statistics.rb | Exposes directory for grouping attribution. |
| test/lib/rails_stats/pack_finder_test.rb | Tests pack/component detection behavior and exclusions. |
| test/lib/rails_stats/stats_calculator_test.rb | Tests grouping behavior, attribution, and totals consistency. |
| test/lib/rails_stats/console_formatter_test.rb | Tests grouped console output sections for pack apps. |
| test/fixtures/packwerk_app/package.yml | Fixture root Packwerk marker for pack detection tests. |
| test/fixtures/packwerk_app/app/models/core_model.rb | Core-app fixture code for attribution tests. |
| test/fixtures/packwerk_app/app/controllers/core_controller.rb | Core-app fixture code for attribution tests. |
| test/fixtures/packwerk_app/lib/core_lib.rb | Core-app root lib fixture for core grouping. |
| test/fixtures/packwerk_app/packs/pack1/package.yml | Pack fixture marker to validate Packwerk pack detection. |
| test/fixtures/packwerk_app/packs/pack1/app/models/pack1_model.rb | Pack fixture code for per-pack attribution. |
| test/fixtures/packwerk_app/packs/pack1/app/controllers/pack1_controller.rb | Pack fixture code for per-pack attribution. |
| test/fixtures/packwerk_app/packs/pack1/lib/pack1_lib.rb | Pack fixture lib coverage for non-gem pack root scanning. |
| test/fixtures/packwerk_app/packs/pack1/spec/models/pack1_model_spec.rb | Pack fixture spec for per-pack test attribution. |
| test/fixtures/packwerk_app/packs/pack2/app/models/pack2_model.rb | Convention-only pack fixture code for detection/attribution. |
| test/fixtures/packwerk_app/packs/pack2/test/models/pack2_model_test.rb | Convention-only pack fixture test for per-pack attribution. |
| test/fixtures/packwerk_app/components/billing/app/models/billing_model.rb | Component fixture code for detection/attribution. |
| test/fixtures/packwerk_app/components/billing/lib/billing.rb | Component fixture lib for per-component attribution. |
| README.md | Documents new grouped output behavior and how pack detection works. |
| CHANGELOG.md | Notes the new feature and the path-argument bugfix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def totals_for(group_statistics) | ||
| code = CodeStatisticsCalculator.new | ||
| tests = CodeStatisticsCalculator.new | ||
| grand = CodeStatisticsCalculator.new |
| Dir.glob(File.join(root, "**", "package.yml")).each do |marker_path| | ||
| next if marker_path =~ IGNORED_PATH | ||
|
|
||
| dir = File.absolute_path(File.dirname(marker_path)) | ||
| next if dir == root | ||
|
|
||
| packs[dir] = true | ||
| end | ||
| end | ||
|
|
||
| def collect_convention_packs(root, packs) | ||
| PACK_CONTAINERS.each do |container| | ||
| Dir.glob(File.join(root, "**", container, "*")).each do |path| | ||
| next if path =~ IGNORED_PATH |
The recursive file walker (Util.calculate_directory_statistics) descended
into node_modules and other vendored directories. This had two effects on a
packs/component monolith, where each pack can carry its own
app/webpack/node_modules:
* it crashed the run: a directory named like a source file (e.g.
node_modules/popper.js) matched the file pattern and was opened as a
file, raising Errno::EISDIR.
* when it did complete, it inflated every number by millions of vendored
files.
Fix it at the root, in the single walker: prune traversal into
node_modules/vendor/tmp/log/.git and never treat a directory as a countable
file. The ignored-name list now lives in one place (RailsStats::IGNORED_DIRS);
PackFinder builds its path regex from it and Util matches it by basename.
Measured on a large packs-based application (17 packs), same command,
before vs. after this change:
| Metric | Before | After |
|---------------------|-------------------|-----------|
| Completes run | no (Errno::EISDIR)| yes |
| Total files | 4,903,892 | 5,033 |
| Total LOC | 3,395,731 | 418,681 |
| Webpacks row: files | 52,075 | 696 |
| Webpacks row: lines | 4,447,768 | 54,495 |
| Code:Test ratio | 1:0.1 | 1:1.1 |
|
@etagwerker Ran this branch against a large packs-based monolith (17 packs) to QA the per-pack breakdown. The grouping works well, but the run first crashed and then produced wildly inflated numbers because the file walker descended into each pack's Same command, before vs. after the fix:
|
Closes #23
Description
rails_statsalready picked up files frompacks/*andcomponents/*when you ranrake statsat the root, but it merged everything into one table. There was no way to see how big each pack was, and (per #23) asking for a single pack withrake stats[packs/pack1]from inside a booted app quietly returned stats for the whole app instead.This PR makes
rails_statsunderstand large Rails monoliths built with Packwerk / packs-rails or component-based Rails.What changes
When packs or components are detected,
rake statsprints:== Core Application ==section for everything in the main app== Pack: packs/pack1 ==section for each pack/component== Total (all packs) ==section aggregating the whole monolithThe same breakdown is available in JSON under a
"packs"key. Plain Rails apps (no packs) get the exact same single-table output as before, so this is backwards compatible.How packs are detected
A new
RailsStats::PackFindertreats a directory as a pack when it either:package.ymlfile (the Packwerk marker), orpacks/orcomponents/directory and holds any ofapp,lib,spec, ortest.Nested packs (
packs/a/packs/b) are supported, and each file is attributed to its most specific pack. Packs that are self-contained gems/engines (they have a.gemspec) keep being counted byGemStatistics, so nothing is double-counted.Bug fix
rake stats[packs/pack1]was ignoring its path argument when run inside a booted Rails app, becausetasks.rboverwrote the given path withRails.rootunconditionally. It now only falls back toRails.rootwhen no path is given.Testing
test/fixtures/packwerk_appfixture with a core app, two packs (one Packwerk-marked, one convention-only), and a component.PackFinder, the groupedStatsCalculator#statistics_by_group, and the grouped console output.CHANGELOG.mdunder the "main (unreleased)" heading.I will abide by the code of conduct.
🤖 Generated with Claude Code