Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# main ([unreleased](https://github.com/fastruby/rails_stats/compare/v2.1.0...main))

* [FEATURE: Support component-based and Packwerk (packs) applications with a per-pack and Core breakdown](https://github.com/fastruby/rails_stats/issues/23)
* [BUGFIX: Respect an explicit path argument (e.g. `rake stats[packs/pack1]`) when running inside a booted Rails app](https://github.com/fastruby/rails_stats/issues/23)
* [CHORE: Improve the GH Test Workflow](https://github.com/fastruby/rails_stats/pull/35)
* [BUGFIX: Explicitly set format as text for `Bundler::Stats::CLI` on `ConsoleFormatter`](https://github.com/fastruby/rails_stats/pull/43)

Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,56 @@ Directory: /path/to/app/
$ for dir in /path/to/many/engines/*/; do bundle exec rake stats[$dir]; done
```

### Component-based and Packwerk (packs) applications

RailsStats understands large Rails monoliths that are organized into
[packs](https://github.com/Shopify/packwerk) (via
[packs-rails](https://github.com/rubyatscale/packs-rails)) or
[components](https://cbra.info/) (component-based Rails).

When packs or components are detected, `rake stats` automatically prints a
breakdown per pack/component, a `Core Application` section for everything that
lives in the main app, and a `Total (all packs)` section that aggregates the
whole monolith:

```bash
$ bundle exec rake stats

== Core Application ==
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| Name | Files | Lines | LOC | Classes | Methods | M/C | LOC/M |
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| ... |
+----------------------+---------+---------+---------+---------+---------+-----+-------+

== Pack: packs/pack1 ==
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| ... |
+----------------------+---------+---------+---------+---------+---------+-----+-------+

== Total (all packs) ==
+----------------------+---------+---------+---------+---------+---------+-----+-------+
| ... |
+----------------------+---------+---------+---------+---------+---------+-----+-------+
```

A directory is treated as a pack when it contains a `package.yml` file (the
Packwerk marker) or when it lives directly under a `packs/` or `components/`
directory and holds any of the usual code folders (`app`, `lib`, `spec`,
`test`). Nested packs (`packs/a/packs/b`) are supported.

You can also scope stats to a single pack, even from inside a booted Rails app:

```bash
$ bundle exec rake stats[packs/pack1]
```

The same breakdown is available in JSON under a `"packs"` key:

```bash
$ bundle exec rake stats[.,json]
```

### Within your Rails project

You can also include it within your Rails application to _replace_ the default `rake stats` implementation.
Expand Down
8 changes: 8 additions & 0 deletions lib/rails_stats/all.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
module RailsStats
# Vendored / generated directory names we never descend into or treat as a
# pack. Shared by PackFinder (which builds IGNORED_PATH from it) and Util
# (which matches them by basename while walking the tree).
IGNORED_DIRS = %w[node_modules vendor tmp log .git].freeze
end

require 'rails_stats/stats_calculator'
require 'rails_stats/stats_formatter'
require 'rails_stats/json_formatter'
require 'rails_stats/console_formatter'
require 'rails_stats/inflector'
require 'rails_stats/code_statistics_calculator'
require 'rails_stats/pack_finder'
require 'rails_stats/util'
require 'rails_stats/app_statistics'
require 'rails_stats/test_statistics'
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/app_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class AppStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

def initialize(directory)
@directories = []
Expand Down
52 changes: 42 additions & 10 deletions lib/rails_stats/console_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@ class ConsoleFormatter < StatsFormatter
def to_s
Bundler::Stats::CLI.start(['--format', 'text'])

print_header
sorted_keys = @statistics.keys.sort
sorted_keys.each { |key| print_line(key, @statistics[key]) }
print_splitter

if @grand_total
print_line("Code", @code_total)
print_line("Tests", @tests_total)
print_line("Total", @grand_total)
print_splitter
if calculator.packs?
print_grouped
else
print_flat
end

print_code_test_stats
Expand All @@ -24,6 +18,44 @@ def to_s

private

def print_flat
print_table(@statistics)
end

def print_grouped
@statistics_by_group.each do |group, group_statistics|
puts "== #{group_label(group)} =="
print_table(group_statistics)
puts ""
end

puts "== #{grand_total_label} =="
print_table(@statistics)
end

def group_label(group)
group == RailsStats::StatsCalculator::CORE_GROUP ? "Core Application" : "Pack: #{group}"
end

def grand_total_label
"Total (all packs)"
end

# Prints a full table (header, one line per concept, and Code/Tests/Total
# totals) for the given {concept => CodeStatisticsCalculator} hash.
def print_table(statistics)
code, tests, grand = calculator.totals_for(statistics)

print_header
statistics.keys.sort.each { |key| print_line(key, statistics[key]) }
print_splitter

print_line("Code", code)
print_line("Tests", tests)
print_line("Total", grand)
print_splitter
end

def print_header
print_splitter
puts "| Name | Files | Lines | LOC | Classes | Methods | M/C | LOC/M |"
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/cucumber_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class CucumberStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

def initialize(directory)
@test = true
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/gem_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class GemStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

def initialize(directory)
@test = false
Expand Down
18 changes: 18 additions & 0 deletions lib/rails_stats/json_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def result
@result << stat_hash("Tests", @tests_total).merge(code_test_hash) if @tests_total
@result << stat_hash("Total", @grand_total).merge(code_test_hash) if @grand_total

@result << { "packs" => packs_breakdown } if calculator.packs?

@result << { "schema_stats" => schema_info }
@result << { "polymorphic_stats" => print_polymorphic_stats }

Expand All @@ -29,6 +31,22 @@ def to_s

private

# Per-pack / per-component breakdown, keyed by group name ("Core" plus
# each pack's relative path). Each group lists its concept stats followed
# by Code/Tests/Total rows for that group.
def packs_breakdown
@statistics_by_group.each_with_object({}) do |(group, group_statistics), out|
code, tests, grand = calculator.totals_for(group_statistics)

rows = group_statistics.map { |key, stats| stat_hash(key, stats) }
rows << stat_hash("Code", code).merge("total" => true)
rows << stat_hash("Tests", tests).merge("total" => true)
rows << stat_hash("Total", grand).merge("total" => true)

out[group] = rows
end
end

def code_test_hash
code = calculator.code_loc
tests = calculator.test_loc
Expand Down
67 changes: 67 additions & 0 deletions lib/rails_stats/pack_finder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
module RailsStats
# Detects "packs" (packwerk / packs-rails) and "components" (component-based
# Rails, à la Stephan Hagemann) inside a Rails application so that stats can
# be reported per pack/component plus a "Core" group for everything else.
#
# A directory is considered a pack when either:
#
# * it contains a `package.yml` file (the packwerk marker), or
# * it is a direct child of a `packs/` or `components/` directory and holds
# any of the usual code folders (`app`, `lib`, `spec`, `test`).
#
# Nested packs (e.g. `packs/a/packs/b`) are supported. The root directory
# itself is never reported as a pack.
module PackFinder
extend self

CODE_FOLDERS = %w[app lib spec test].freeze
PACK_CONTAINERS = %w[packs components].freeze

# Path regex used to filter glob results to skip vendored/generated
# directories. Built from the shared RailsStats::IGNORED_DIRS list.
IGNORED_PATH = %r{/(#{IGNORED_DIRS.map { |dir| Regexp.escape(dir) }.join("|")})/}.freeze

# Returns the absolute paths of every detected pack/component, sorted.
def find(root_directory)
root = File.absolute_path(root_directory)
packs = {}

collect_packwerk_packs(root, packs)
collect_convention_packs(root, packs)

packs.keys.sort
end

private

def collect_packwerk_packs(root, packs)
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
Comment on lines +38 to +51
next unless File.directory?(path)

dir = File.absolute_path(path)
next if dir == root
next unless code_folder?(dir)

packs[dir] = true
end
end
end

def code_folder?(dir)
CODE_FOLDERS.any? { |folder| File.directory?(File.join(dir, folder)) }
end
end
end
2 changes: 1 addition & 1 deletion lib/rails_stats/root_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class RootStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

ROOT_FOLDERS = {
"lib" => "Libraries",
Expand Down
2 changes: 1 addition & 1 deletion lib/rails_stats/spec_statistics.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module RailsStats
class SpecStatistics
attr_reader :statistics, :total, :test
attr_reader :statistics, :total, :test, :directory

SPEC_FOLDERS = ['controllers',
'features',
Expand Down
Loading
Loading