Skip to content

Commit fa46a24

Browse files
Allow packwerk to scan for packages inside of engines
This commit modifies packwerk so that it can scan for packages defined in any engines loaded in a given rails application. This allows support for more complex repository structures where the rails app is not at the root of the repository, and where that app depends on engines defined in sibiling directories. To support this, I modified `PackageSet` so it also looks at `Rails.application.railties` to pull a list of engines, and their root directories, and include those in the paths scanned for packages. I also dropped the restrictions on load paths for constant resolution in `ApplicationLoadPaths`. The result is that packwerk can now find packages defined in these engines, and resolve constants in these engines. With this setup, packwerk still only scans files in the actual app and it's subdirectories for violations, but now it can resolve constants in the engines, and attach them to packages in the engine. For testing, I created a new fixture which models this sort of directory structure, and mocks an app with a rails engine.
1 parent d5b07fa commit fa46a24

File tree

24 files changed

+148
-32
lines changed

24 files changed

+148
-32
lines changed

lib/packwerk/application_load_paths.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,9 @@ def extract_application_autoload_paths
3131
end
3232
def filter_relevant_paths(all_paths, bundle_path: Bundler.bundle_path, rails_root: Rails.root)
3333
bundle_path_match = bundle_path.join("**")
34-
rails_root_match = rails_root.join("**")
3534

3635
all_paths
3736
.transform_keys { |path| Pathname.new(path).expand_path }
38-
.select { |path| path.fnmatch(rails_root_match.to_s) } # path needs to be in application directory
3937
.reject { |path| path.fnmatch(bundle_path_match.to_s) } # reject paths from vendored gems
4038
end
4139

lib/packwerk/application_validator.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def package_manifests(glob_pattern = package_glob)
284284
@configuration.root_path,
285285
glob_pattern,
286286
@configuration.exclude,
287+
@configuration.packages_outside_of_app_dir_enabled
287288
)
288289
package_paths.all_paths.map { |f| File.realpath(f) }
289290
end

lib/packwerk/configuration.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def from_packwerk_config(path)
4141
:custom_associations,
4242
:config_path,
4343
:cache_directory,
44+
:packages_outside_of_app_dir_enabled,
4445
)
4546

4647
def initialize(configs = {}, config_path: nil)
@@ -54,6 +55,7 @@ def initialize(configs = {}, config_path: nil)
5455
@cache_enabled = configs.key?("cache") ? configs["cache"] : false
5556
@cache_directory = Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk")
5657
@config_path = config_path
58+
@packages_outside_of_app_dir_enabled = configs["packages_outside_of_app_dir_enabled"] || false
5759

5860
if configs["load_paths"]
5961
warning = <<~WARNING

lib/packwerk/package_paths.rb

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,26 @@ class PackagePaths
1717
root_path: String,
1818
package_pathspec: T.nilable(PathSpec),
1919
exclude_pathspec: T.nilable(PathSpec),
20+
scan_for_packages_outside_of_app_dir: T.nilable(T::Boolean)
2021
).void
2122
end
22-
def initialize(root_path, package_pathspec, exclude_pathspec = nil)
23+
def initialize(root_path, package_pathspec, exclude_pathspec = nil, scan_for_packages_outside_of_app_dir = false)
2324
@root_path = root_path
2425
@package_pathspec = package_pathspec
2526
@exclude_pathspec = exclude_pathspec
27+
@scan_for_packages_outside_of_app_dir = scan_for_packages_outside_of_app_dir
2628
end
2729

2830
def all_paths
2931
exclude_pathspec = Array(@exclude_pathspec).dup
3032
.push(Bundler.bundle_path.join("**").to_s)
3133
.map { |glob| File.expand_path(glob) }
3234

33-
paths_to_scan = [@root_path]
35+
paths_to_scan = if @scan_for_packages_outside_of_app_dir
36+
engine_paths_to_scan.push(@root_path)
37+
else
38+
[@root_path]
39+
end
3440

3541
glob_patterns = paths_to_scan.product(Array(@package_pathspec)).map do |path, pathspec|
3642
File.join(path, pathspec, PACKAGE_CONFIG_FILENAME)
@@ -49,5 +55,16 @@ def exclude_path?(globs, path)
4955
path.realpath.fnmatch(glob, File::FNM_EXTGLOB)
5056
end
5157
end
58+
59+
sig { returns(T::Array[String]) }
60+
def engine_paths_to_scan
61+
bundle_path_match = Bundler.bundle_path.join("**")
62+
63+
Rails.application.railties
64+
.select { |r| r.is_a?(Rails::Engine) }
65+
.map { |r| Pathname.new(r.root).expand_path }
66+
.reject { |path| path.fnmatch(bundle_path_match.to_s) } # reject paths from vendored gems
67+
.map(&:to_s)
68+
end
5269
end
5370
end

lib/packwerk/package_set.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ class << self
1919
extend T::Sig
2020

2121
sig do
22-
params(root_path: String, package_pathspec: T.nilable(PackagePaths::PathSpec)).returns(PackageSet)
22+
params(root_path: String, package_pathspec: T.nilable(PackagePaths::PathSpec),
23+
scan_for_packages_outside_of_app_dir: T.nilable(T::Boolean)).returns(PackageSet)
2324
end
24-
def load_all_from(root_path, package_pathspec: nil)
25-
package_paths = PackagePaths.new(root_path, package_pathspec || "**", nil)
25+
def load_all_from(root_path, package_pathspec: nil, scan_for_packages_outside_of_app_dir: false)
26+
package_paths = PackagePaths.new(root_path, package_pathspec || "**", nil, scan_for_packages_outside_of_app_dir)
2627

2728
packages = package_paths.all_paths.map do |path|
2829
root_relative = path.dirname.relative_path_from(root_path)

lib/packwerk/run_context.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def from_configuration(configuration)
2626
root_path: configuration.root_path,
2727
load_paths: configuration.load_paths,
2828
package_paths: configuration.package_paths,
29+
packages_outside_of_app_dir_enabled: configuration.packages_outside_of_app_dir_enabled,
2930
inflector: inflector,
3031
custom_associations: configuration.custom_associations,
3132
cache_enabled: configuration.cache_enabled?,
@@ -46,6 +47,7 @@ def from_configuration(configuration)
4647
custom_associations: AssociationInspector::CustomAssociations,
4748
checkers: T::Array[ReferenceChecking::Checkers::Checker],
4849
cache_enabled: T::Boolean,
50+
packages_outside_of_app_dir_enabled: T.nilable(T::Boolean),
4951
).void
5052
end
5153
def initialize(
@@ -57,7 +59,8 @@ def initialize(
5759
package_paths: nil,
5860
custom_associations: [],
5961
checkers: DEFAULT_CHECKERS,
60-
cache_enabled: false
62+
cache_enabled: false,
63+
packages_outside_of_app_dir_enabled: false
6164
)
6265
@root_path = root_path
6366
@load_paths = load_paths
@@ -68,6 +71,7 @@ def initialize(
6871
@cache_enabled = cache_enabled
6972
@cache_directory = cache_directory
7073
@config_path = config_path
74+
@packages_outside_of_app_dir_enabled = packages_outside_of_app_dir_enabled
7175
@file_processor = T.let(nil, T.nilable(FileProcessor))
7276
@context_provider = T.let(nil, T.nilable(ConstantDiscovery))
7377
# We need to initialize this before we fork the process, see https://github.com/Shopify/packwerk/issues/182
@@ -127,6 +131,7 @@ def package_set
127131
::Packwerk::PackageSet.load_all_from(
128132
@root_path,
129133
package_pathspec: @package_paths,
134+
scan_for_packages_outside_of_app_dir: @packages_outside_of_app_dir_enabled,
130135
)
131136
end
132137

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# typed: ignore
2+
# frozen_string_literal: true
3+
4+
module HasTimeline
5+
end

test/fixtures/external_packages/app/components/timeline/app/models/imaginary/.gitkeep

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class PrivateThing
2+
end

test/fixtures/external_packages/app/components/timeline/app/models/sales/.gitkeep

Whitespace-only changes.

0 commit comments

Comments
 (0)