Skip to content
Open
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
19 changes: 10 additions & 9 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,16 @@ Secondly, to enable Spring, first run `bin/spring binstub packwerk` which will "

Packwerk reads from the `packwerk.yml` configuration file in the root directory. Packwerk will run with the default configuration if any of these settings are not specified.

| Key | Default value | Description |
|----------------------|-------------------------------------------|--------------|
| include | **/*.{rb,rake,erb} | list of patterns for folder paths to include |
| exclude | {bin,node_modules,script,tmp,vendor}/**/* | list of patterns for folder paths to exclude |
| package_paths | **/ | a single pattern or a list of patterns to find package configuration files, see: [Defining packages](#Defining-packages) |
| custom_associations | N/A | list of custom associations, if any |
| parallel | true | when true, fork code parsing out to subprocesses |
| cache | false | when true, caches the results of parsing files |
| cache_directory | tmp/cache/packwerk | the directory that will hold the packwerk cache |
| Key | Default value | Description |
|-------------------------------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| include | **/*.{rb,rake,erb} | list of patterns for folder paths to include |
| exclude | {bin,node_modules,script,tmp,vendor}/**/* | list of patterns for folder paths to exclude |
| package_paths | **/ | a single pattern or a list of patterns to find package configuration files, see: [Defining packages](#Defining-packages) |
| custom_associations | N/A | list of custom associations, if any |
| parallel | true | when true, fork code parsing out to subprocesses |
| cache | false | when true, caches the results of parsing files |
| cache_directory | tmp/cache/packwerk | the directory that will hold the packwerk cache |
| packages_outside_of_app_dir_enabled | false | when true, packwerk will scan for packages in folders for locally published rails engines |

### Using a custom ERB parser

Expand Down
2 changes: 2 additions & 0 deletions lib/packwerk.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ module Packwerk
autoload :Package
autoload :PackageSet
autoload :PackageTodo
autoload :PackagePaths
autoload :ParsedConstantDefinitions
autoload :Parsers
autoload :RailsLoadPaths
autoload :Reference
Expand Down
4 changes: 4 additions & 0 deletions lib/packwerk/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ def from_packwerk_config(path)
sig { returns(Pathname) }
attr_reader(:cache_directory)

sig { returns(T::Boolean) }
attr_reader(:packages_outside_of_app_dir_enabled)

sig { params(parallel: T::Boolean).returns(T::Boolean) }
attr_writer(:parallel)

Expand All @@ -80,6 +83,7 @@ def initialize(configs = {}, config_path: nil)
@cache_enabled = T.let(configs.key?("cache") ? configs["cache"] : false, T::Boolean)
@cache_directory = T.let(Pathname.new(configs["cache_directory"] || "tmp/cache/packwerk"), Pathname)
@config_path = config_path
@packages_outside_of_app_dir_enabled = configs["packages_outside_of_app_dir_enabled"] || false

@offenses_formatter_identifier = T.let(
configs["offenses_formatter"] || Formatters::DefaultOffensesFormatter::IDENTIFIER, String
Expand Down
71 changes: 71 additions & 0 deletions lib/packwerk/package_paths.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# typed: strict
# frozen_string_literal: true

require "pathname"
require "bundler"

module Packwerk
class PackagePaths
PACKAGE_CONFIG_FILENAME = "package.yml"
PathSpec = T.type_alias { T.any(String, T::Array[String]) }

extend T::Sig
extend T::Generic

sig do
params(
root_path: String,
package_pathspec: T.nilable(PathSpec),
exclude_pathspec: T.nilable(PathSpec),
scan_for_packages_outside_of_app_dir: T.nilable(T::Boolean)
).void
end
def initialize(root_path, package_pathspec, exclude_pathspec = nil, scan_for_packages_outside_of_app_dir = false)
@root_path = root_path
@package_pathspec = package_pathspec
@exclude_pathspec = exclude_pathspec
@scan_for_packages_outside_of_app_dir = scan_for_packages_outside_of_app_dir
end

sig {returns(T::Array[Pathname])}
def all_paths
exclude_pathspec = Array(@exclude_pathspec).dup
.push(Bundler.bundle_path.join("**").to_s)
.map { |glob| File.expand_path(glob) }

paths_to_scan = if @scan_for_packages_outside_of_app_dir
engine_paths_to_scan.push(@root_path)
else
[@root_path]
end

glob_patterns = paths_to_scan.product(Array(@package_pathspec)).map do |path, pathspec|
File.join(path, pathspec, PACKAGE_CONFIG_FILENAME)
end

Dir.glob(glob_patterns)
.map { |path| Pathname.new(path).cleanpath }
.reject { |path| exclude_path?(exclude_pathspec, path) }
end

private

sig { params(globs: T::Array[String], path: Pathname).returns(T::Boolean) }
def exclude_path?(globs, path)
globs.any? do |glob|
path.realpath.fnmatch(glob, File::FNM_EXTGLOB)
end
end

sig { returns(T::Array[String]) }
def engine_paths_to_scan
bundle_path_match = Bundler.bundle_path.join("**")

Rails.application.railties
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't depend on railties in the gemspec so I'd rather not depend on rails constants here. I think this also assumes all local engines are packages. Couldn't you just glob for package.yml files instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't depend on railties in the gemspec so I'd rather not depend on rails constants here

I'll address this in your second comment below about paths because I think they're kind of related

I think this also assumes all local engines are packages

That's not exactly accurate. It assumes any local engine might contain packages, but not that the engine itself is a package. The engine doesn't have to contain packages either. It's just adding all the paths to the list of paths that later get scanned for via glob for a package.yml file.

.select { |r| r.is_a?(Rails::Engine) }
.map { |r| Pathname.new(r.root).expand_path }
.reject { |path| path.fnmatch(bundle_path_match.to_s) } # reject paths from vendored gems
.map(&:to_s)
end
end
end
40 changes: 8 additions & 32 deletions lib/packwerk/package_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
require "bundler"

module Packwerk
PathSpec = T.type_alias { T.any(String, T::Array[String]) }

# A set of {Packwerk::Package}s as well as methods to parse packages from the filesystem.
class PackageSet
Expand All @@ -20,11 +19,14 @@ class PackageSet
class << self
extend T::Sig

sig { params(root_path: String, package_pathspec: T.nilable(PathSpec)).returns(PackageSet) }
def load_all_from(root_path, package_pathspec: nil)
package_paths = package_paths(root_path, package_pathspec || "**")
sig do
params(root_path: String, package_pathspec: T.nilable(PackagePaths::PathSpec),
scan_for_packages_outside_of_app_dir: T.nilable(T::Boolean)).returns(PackageSet)
end
def load_all_from(root_path, package_pathspec: nil, scan_for_packages_outside_of_app_dir: false)
package_paths = PackagePaths.new(root_path, package_pathspec || "**", nil, scan_for_packages_outside_of_app_dir)

packages = package_paths.map do |path|
packages = package_paths.all_paths.map do |path|
root_relative = path.dirname.relative_path_from(root_path)
Package.new(name: root_relative.to_s, config: YAML.load_file(path, fallback: nil))
end
Expand All @@ -34,27 +36,6 @@ def load_all_from(root_path, package_pathspec: nil)
new(packages)
end

sig do
params(
root_path: String,
package_pathspec: PathSpec,
exclude_pathspec: T.nilable(PathSpec)
).returns(T::Array[Pathname])
end
def package_paths(root_path, package_pathspec, exclude_pathspec = [])
exclude_pathspec = Array(exclude_pathspec).dup
.push(Bundler.bundle_path.join("**").to_s)
.map { |glob| File.expand_path(glob) }

glob_patterns = Array(package_pathspec).map do |pathspec|
File.join(root_path, pathspec, PACKAGE_CONFIG_FILENAME)
end

Dir.glob(glob_patterns)
.map { |path| Pathname.new(path).cleanpath }
.reject { |path| exclude_path?(exclude_pathspec, path) }
end

private

sig { params(packages: T::Array[Package]).void }
Expand All @@ -64,12 +45,7 @@ def create_root_package_if_none_in(packages)
packages << Package.new(name: Package::ROOT_PACKAGE_NAME, config: nil)
end

sig { params(globs: T::Array[String], path: Pathname).returns(T::Boolean) }
def exclude_path?(globs, path)
globs.any? do |glob|
path.realpath.fnmatch(glob, File::FNM_EXTGLOB)
end
end

end

sig { returns(T::Hash[String, Package]) }
Expand Down
2 changes: 0 additions & 2 deletions lib/packwerk/rails_load_paths.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ def extract_application_autoload_paths
end
def filter_relevant_paths(all_paths, bundle_path: Bundler.bundle_path, rails_root: Rails.root)
bundle_path_match = bundle_path.join("**")
rails_root_match = rails_root.join("**")

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

Expand Down
20 changes: 16 additions & 4 deletions lib/packwerk/run_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def from_configuration(configuration)
root_path: configuration.root_path,
load_paths: configuration.load_paths,
package_paths: configuration.package_paths,
packages_outside_of_app_dir_enabled: configuration.packages_outside_of_app_dir_enabled,
inflector: inflector,
custom_associations: configuration.custom_associations,
cache_enabled: configuration.cache_enabled?,
Expand All @@ -41,6 +42,7 @@ def from_configuration(configuration)
custom_associations: AssociationInspector::CustomAssociations,
checkers: T::Array[Checker],
cache_enabled: T::Boolean,
packages_outside_of_app_dir_enabled: T.nilable(T::Boolean),
).void
end
def initialize(
Expand All @@ -52,7 +54,8 @@ def initialize(
package_paths: nil,
custom_associations: [],
checkers: Checker.all,
cache_enabled: false
cache_enabled: false,
packages_outside_of_app_dir_enabled: false
)
@root_path = root_path
@load_paths = load_paths
Expand All @@ -63,7 +66,7 @@ def initialize(
@cache_enabled = cache_enabled
@cache_directory = cache_directory
@config_path = config_path

@packages_outside_of_app_dir_enabled = packages_outside_of_app_dir_enabled
@file_processor = T.let(nil, T.nilable(FileProcessor))
@context_provider = T.let(nil, T.nilable(ConstantDiscovery))
@package_set = T.let(nil, T.nilable(PackageSet))
Expand All @@ -77,18 +80,27 @@ def initialize(
def process_file(relative_file:)
processed_file = file_processor.call(relative_file)

puts relative_file

references = ReferenceExtractor.get_fully_qualified_references_from(
processed_file.unresolved_references,
context_provider
)
puts references
reference_checker = ReferenceChecking::ReferenceChecker.new(@checkers)

processed_file.offenses + references.flat_map { |reference| reference_checker.call(reference) }
end

sig { returns(PackageSet) }
def package_set
@package_set ||= ::Packwerk::PackageSet.load_all_from(@root_path, package_pathspec: @package_paths)
@package_set ||= ::Packwerk::PackageSet.load_all_from(
@root_path,
package_pathspec: @package_paths,
scan_for_packages_outside_of_app_dir: @packages_outside_of_app_dir_enabled,
)
puts @package_set.packages
@package_set
end

private
Expand Down Expand Up @@ -123,7 +135,7 @@ def resolver
inflector: @inflector,
)
end

sig { returns(T::Array[ConstantNameInspector]) }
def constant_name_inspectors
[
Expand Down
9 changes: 7 additions & 2 deletions lib/packwerk/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ def package_manifests_settings_for(configuration, setting)
end
def package_manifests(configuration, glob_pattern = nil)
glob_pattern ||= package_glob(configuration)
PackageSet.package_paths(configuration.root_path, glob_pattern, configuration.exclude)
.map { |f| File.realpath(f) }
package_paths = PackagePaths.new(
configuration.root_path,
glob_pattern,
configuration.exclude,
configuration.packages_outside_of_app_dir_enabled
)
package_paths.all_paths.map { |f| File.realpath(f) }
end

sig { params(configuration: Configuration).returns(T.any(T::Array[String], String)) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# typed: ignore
# frozen_string_literal: true

module HasTimeline
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class PrivateThing
end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enforce_dependencies: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
enforce_privacy:
- "::PrivateThing"
Empty file.
Empty file.
1 change: 1 addition & 0 deletions test/fixtures/external_packages/app/packwerk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages_outside_of_app_dir_enabled: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# typed: ignore
# frozen_string_literal: true

class Order
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# typed: ignore
# frozen_string_literal: true

module Sales
class Order
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# typed: ignore
# frozen_string_literal: true

module Sales
module RecordNewOrder
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
enforce_privacy: true

metadata:
stewards:
- "@Shopify/sales"
slack_channels:
- "#sales"
Loading