Class: Rigor::ModuleGraph::PackwerkOverlay

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/module_graph/packwerk_overlay.rb

Overview

Discovers Packwerk-style packages (‘package.yml`) inside a project tree and maps source file paths to their owning package.

Treats every directory that contains a package.yml as a package root. The package’s name is its path relative to the project root with a leading ./ stripped — that’s how packwerk itself reports them, and it’s stable across Packwerk versions which gives the renderer something to use as the cluster label.

Files map to the deepest ancestor package — if a nested ‘packages/billing/invoices/package.yml` lives under `packages/billing/package.yml`, a file under the inner one belongs to packages/billing/invoices, not packages/billing.

Defined Under Namespace

Classes: Package

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project_root:, packages:) ⇒ PackwerkOverlay

Returns a new instance of PackwerkOverlay.

Parameters:

  • project_root (String)

    the project root the packages are reported relative to

  • packages (Array<Package>)

    frozen



38
39
40
41
42
43
44
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 38

def initialize(project_root:, packages:)
  @project_root = realpath_or_expand(project_root)
  @packages = packages
              .map { |pkg| Package.new(name: pkg.name, root: realpath_or_expand(pkg.root)) }
              .sort_by { |p| -p.root.length }
              .freeze
end

Instance Attribute Details

#packagesObject (readonly)

Returns the value of attribute packages.



33
34
35
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 33

def packages
  @packages
end

#project_rootObject (readonly)

Returns the value of attribute project_root.



33
34
35
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 33

def project_root
  @project_root
end

Class Method Details

.discover(project_root) ⇒ PackwerkOverlay

Parameters:

  • project_root (String)

Returns:



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 48

def self.discover(project_root)
  root = File.expand_path(project_root)
  packages = []
  Find.find(root) do |path|
    base = File.basename(path)
    if File.directory?(path) && EXCLUDED_DIRS.include?(base) && path != root
      Find.prune
      next
    end
    next unless File.file?(path) && base == "package.yml"

    pkg_root = File.dirname(path)
    packages << Package.new(name: package_name(pkg_root, root), root: pkg_root)
  end
  new(project_root: root, packages: packages)
end

.package_name(pkg_root, project_root) ⇒ Object



65
66
67
68
69
70
71
72
73
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 65

def self.package_name(pkg_root, project_root)
  # The root package (a `package.yml` at the project root) is
  # canonically called `.` in Packwerk output. Match that so
  # users see the familiar label.
  return "." if pkg_root == project_root

  rel = pkg_root.sub(%r{\A#{Regexp.escape(project_root)}/?}, "")
  rel.empty? ? "." : rel
end

Instance Method Details

#any?Boolean

Returns true when at least one package.yml was found.

Returns:

  • (Boolean)

    true when at least one package.yml was found.



77
78
79
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 77

def any?
  !@packages.empty?
end

#groups_for(edges) ⇒ Object

Build a {node_name => package_name} mapping from a list of edges. We only attribute a node to a package when we have evidence the node is declared under that package’s root — that is, the node appears as edge.from for at least one edge, and that edge’s path lives under the package. The to side is just a reference; using it would mis-attribute base classes (ApplicationRecord) and any other external constant to whichever package happens to reference them first.



131
132
133
134
135
136
137
138
139
140
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 131

def groups_for(edges)
  node_paths = {}
  edges.each do |edge|
    node_paths[edge.from] ||= edge.path
  end
  node_paths.each_with_object({}) do |(name, path), acc|
    pkg = package_for(path)
    acc[name] = pkg.name if pkg
  end
end

#package_for(path) ⇒ Object

Find the deepest package whose root is an ancestor of path. Returns nil when the path is outside every package’s root.

Both sides are normalised through realpath when possible so a macOS /tmp/private/tmp symlink (or any other symlink in the project root path) doesn’t make the comparison spuriously miss.



89
90
91
92
93
94
95
96
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 89

def package_for(path)
  return nil if path.nil? || path.empty?

  absolute = realpath_of(File.expand_path(path, @project_root))
  @packages.find do |pkg|
    absolute == pkg.root || absolute.start_with?(pkg.root + "/")
  end
end

#realpath_of(path) ⇒ Object



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 104

def realpath_of(path)
  File.realpath(path)
rescue Errno::ENOENT
  # Tests and synthetic edges may carry paths whose tail
  # doesn't exist on disk; walk up to the deepest existing
  # ancestor, realpath that, then reattach the missing tail.
  # That makes a macOS +/tmp+ ↔ +/private/tmp+ symlink
  # transparent even for synthetic paths.
  parent = path
  until parent == File.dirname(parent)
    parent = File.dirname(parent)
    if File.exist?(parent)
      return File.realpath(parent) + path[parent.length..]
    end
  end
  path
end

#realpath_or_expand(path) ⇒ Object



98
99
100
101
102
# File 'lib/rigor/module_graph/packwerk_overlay.rb', line 98

def realpath_or_expand(path)
  File.realpath(File.expand_path(path))
rescue Errno::ENOENT
  File.expand_path(path)
end