bundler-why-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6e84117a1e8c9c8e87564646be2035584aee1d907a283fa353f56079e7fa81a
4
+ data.tar.gz: 8491724b3eea6c326131ec854063daee113b032a5a62b81201e35e8ad5520e9f
5
+ SHA512:
6
+ metadata.gz: 5d7f2c7e89d464d29f8bb7d471d63debd45137c71fc26d4f9f8b5dfca2366053c9b540e498a4df7393c393f4a2499cdd2e868f6e1d5a29802067684994c7ea20
7
+ data.tar.gz: 140c8a23e42241cb808fa8056c52cecfb59853f31cf5765b2e1db0a5d2bad2736491fa494324a194bcae490c7d49e8069893fbe51cf0320282c0091e2f677651
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Hiroshi SHIBATA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # Bundler Why
2
+
3
+ A Bundler plugin that shows why a specific package is installed in your Ruby project, similar to `yarn why` in Yarn/npm.
4
+
5
+ This gem is a Bundler plugin that helps you understand the dependency tree of your gems and why a particular gem is required by your project.
6
+
7
+ ## Installation
8
+
9
+ Install this plugin by running:
10
+
11
+ ```bash
12
+ bundle plugin install bundler-why-plugin
13
+ ```
14
+
15
+ To install from a local path during development:
16
+
17
+ ```bash
18
+ bundle plugin install bundler-why-plugin --source /path/to/bundler-why-plugin
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ To see why a specific package is installed:
24
+
25
+ ```bash
26
+ bundle why <package_name>
27
+ ```
28
+
29
+ ### Example
30
+
31
+ ```bash
32
+ $ bundle why json
33
+ json (2.7.0)
34
+
35
+ Directly required by:
36
+ bundler (2.5.0) [>= 0]
37
+
38
+ Location: /Users/hsbt/.gem/ruby/3.3.0/gems/json-2.7.0/lib/json.rb
39
+ ```
40
+
41
+ ## How It Works
42
+
43
+ The `bundle why` command performs the following:
44
+
45
+ 1. **Parses your Gemfile.lock**: Loads the current bundle configuration
46
+ 2. **Analyzes dependencies**: Traces which gems depend on the specified package
47
+ 3. **Displays results**: Shows:
48
+ - The gem's version number
49
+ - Direct dependents (gems that directly require it)
50
+ - All dependents (both direct and indirect)
51
+ - The gem's installation location
52
+
53
+ ## Features
54
+
55
+ - Shows direct dependents of a package
56
+ - Traces the full dependency chain
57
+ - Displays gem versions and requirement specifiers
58
+ - Handles both direct and transitive dependencies
59
+ - Clear, Yarn-like output format
60
+
61
+ ## Development
62
+
63
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
64
+
65
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
66
+
67
+ ## Contributing
68
+
69
+ Bug reports and pull requests are welcome on GitHub at https://github.com/hsbt/bundler-why-plugin.
70
+
71
+ ## License
72
+
73
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "bundler/why/dependency_resolver"
5
+
6
+ module Bundler
7
+ module Why
8
+ class CLI < Thor
9
+ desc "why PACKAGE", "Show why a specific package is installed"
10
+ def why(package_name = nil)
11
+ if package_name.nil?
12
+ Bundler.ui.error("Error: Please specify a package name")
13
+ Bundler.ui.error("Usage: bundle why <package_name>")
14
+ exit 1
15
+ end
16
+
17
+ resolver = DependencyResolver.new
18
+ result = resolver.analyze(package_name)
19
+
20
+ unless result
21
+ Bundler.ui.error("Error: Package '#{package_name}' not found in Gemfile.lock")
22
+ exit 1
23
+ end
24
+
25
+ display_result(result, resolver)
26
+ end
27
+
28
+ def self.exit_on_failure?
29
+ true
30
+ end
31
+
32
+ private
33
+
34
+ def display_result(result, resolver)
35
+ Bundler.ui.info("#{result[:name]} (#{result[:version]})")
36
+ Bundler.ui.info("")
37
+
38
+ # 依存関係チェーンを表示
39
+ chains = resolver.find_dependency_chain(result[:name])
40
+
41
+ if chains.any?
42
+ Bundler.ui.info("Used by:")
43
+ chains.each do |chain|
44
+ chain_str = chain.join(" > ")
45
+ Bundler.ui.info(" #{chain_str}")
46
+ end
47
+ else
48
+ # 直接の依存元を表示
49
+ direct_dependents = result[:direct_dependents]
50
+ if direct_dependents.any?
51
+ Bundler.ui.info("Directly required by:")
52
+ direct_dependents.each do |dependent|
53
+ Bundler.ui.info(" #{dependent[:name]} (#{dependent[:version]}) [#{dependent[:requirement]}]")
54
+ end
55
+ else
56
+ Bundler.ui.info("Required by:")
57
+ all_dependents = result[:all_dependents]
58
+ if all_dependents.any?
59
+ all_dependents.each do |dependent|
60
+ Bundler.ui.info(" #{dependent[:name]} (#{dependent[:version]})")
61
+ end
62
+ else
63
+ Bundler.ui.warn("Not required by any other packages (may be a direct dependency)")
64
+ end
65
+ end
66
+ end
67
+
68
+ Bundler.ui.info("")
69
+ if result[:path]
70
+ Bundler.ui.info("Location: #{result[:path]}")
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+
5
+ module Bundler
6
+ module Why
7
+ class DependencyResolver
8
+ def initialize
9
+ @definition = Bundler.definition
10
+ @specs = @definition.specs
11
+ end
12
+
13
+ # 指定されたパッケージがなぜ必要かを分析
14
+ def analyze(package_name)
15
+ spec = find_spec(package_name)
16
+ return nil unless spec
17
+
18
+ {
19
+ name: spec.name,
20
+ version: spec.version.to_s,
21
+ direct_dependents: find_direct_dependents(spec),
22
+ all_dependents: find_all_dependents(spec),
23
+ path: spec.loaded_from
24
+ }
25
+ end
26
+
27
+ # 指定されたパッケージを検索
28
+ def find_spec(package_name)
29
+ @specs.find { |spec| spec.name == package_name || spec.name.downcase == package_name.downcase }
30
+ end
31
+
32
+ # 直接的な依存元(親)を取得
33
+ def find_direct_dependents(spec)
34
+ dependents = []
35
+
36
+ @specs.each do |other_spec|
37
+ other_spec.dependencies.each do |dep|
38
+ if dep.name == spec.name
39
+ dependents << {
40
+ name: other_spec.name,
41
+ version: other_spec.version.to_s,
42
+ requirement: dep.requirement.to_s
43
+ }
44
+ end
45
+ end
46
+ end
47
+
48
+ dependents
49
+ end
50
+
51
+ # すべての依存元(直接・間接)を取得
52
+ def find_all_dependents(spec)
53
+ dependents = Set.new
54
+ queue = [spec.name]
55
+ visited = Set.new
56
+
57
+ while queue.any?
58
+ current_name = queue.shift
59
+ next if visited.include?(current_name)
60
+ visited.add(current_name)
61
+
62
+ @specs.each do |other_spec|
63
+ other_spec.dependencies.each do |dep|
64
+ if dep.name == current_name
65
+ dependents.add(other_spec.name)
66
+ queue << other_spec.name unless visited.include?(other_spec.name)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ dependents.map do |name|
73
+ dep_spec = find_spec(name)
74
+ {
75
+ name: name,
76
+ version: dep_spec&.version.to_s
77
+ }
78
+ end
79
+ end
80
+
81
+ # 依存関係チェーンを取得
82
+ def find_dependency_chain(target_name)
83
+ spec = find_spec(target_name)
84
+ return nil unless spec
85
+
86
+ chains = []
87
+
88
+ # Gemfileの直接の依存関係を確認
89
+ @definition.dependencies.each do |dep|
90
+ if is_dependency_of?(target_name, dep.name)
91
+ chain = trace_chain(dep.name, target_name, [dep.name])
92
+ chains << chain if chain
93
+ end
94
+ end
95
+
96
+ chains
97
+ end
98
+
99
+ private
100
+
101
+ # targetが sourceの依存関係にあるかチェック
102
+ def is_dependency_of?(target, source)
103
+ source_spec = find_spec(source)
104
+ return false unless source_spec
105
+
106
+ source_spec.dependencies.any? { |dep| dep.name == target }
107
+ end
108
+
109
+ # 依存関係チェーンをトレース
110
+ def trace_chain(current, target, path)
111
+ current_spec = find_spec(current)
112
+ return nil unless current_spec
113
+
114
+ return path if current == target
115
+
116
+ current_spec.dependencies.each do |dep|
117
+ if is_dependency_of?(target, dep.name)
118
+ chain = trace_chain(dep.name, target, path + [dep.name])
119
+ return chain if chain
120
+ end
121
+ end
122
+
123
+ nil
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Why
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require_relative "why/version"
5
+ require_relative "why/cli"
6
+ require_relative "why/dependency_resolver"
7
+
8
+ module Bundler
9
+ module Why
10
+ class Error < StandardError; end
11
+ end
12
+ end
data/plugins.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "bundler"
2
+ require "bundler/why"
3
+
4
+ module Bundler
5
+ module Why
6
+ class Plugin < ::Bundler::Plugin::API
7
+ command "why"
8
+
9
+ def exec(command_name, args)
10
+ ::Bundler::Why::CLI.start(args)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ module Bundler
2
+ module Why
3
+ VERSION: String
4
+
5
+ class Error < StandardError
6
+ end
7
+
8
+ class DependencyResolver
9
+ def initialize: () -> void
10
+ def analyze: (String package_name) -> { name: String, version: String, direct_dependents: Array[Hash[String, String]], all_dependents: Array[Hash[String, String]], path: String }?
11
+ def find_spec: (String package_name) -> ::Gem::Specification?
12
+ def find_direct_dependents: (::Gem::Specification spec) -> Array[Hash[String, String]]
13
+ def find_all_dependents: (::Gem::Specification spec) -> Array[Hash[String, String]]
14
+ def find_dependency_chain: (String target_name) -> Array[Array[String]]
15
+ end
16
+
17
+ class CLI < Thor
18
+ def why: (String? package_name) -> void
19
+ end
20
+
21
+ class Plugin < ::Bundler::Plugin::API
22
+ def exec: (String command_name, Array[String] args) -> void
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bundler-why-plugin
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hiroshi SHIBATA
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 2.4.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 2.4.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: thor
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '1.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '1.0'
40
+ description: A Bundler plugin that shows the dependency tree for a specific package,
41
+ similar to 'yarn why' in Yarn.
42
+ email:
43
+ - hsbt@ruby-lang.org
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE.txt
49
+ - README.md
50
+ - Rakefile
51
+ - lib/bundler/why.rb
52
+ - lib/bundler/why/cli.rb
53
+ - lib/bundler/why/dependency_resolver.rb
54
+ - lib/bundler/why/version.rb
55
+ - plugins.rb
56
+ - sig/bundler/why.rbs
57
+ homepage: https://github.com/hsbt/bundler-why-plugin
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/hsbt/bundler-why-plugin
62
+ source_code_uri: https://github.com/hsbt/bundler-why-plugin
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 3.2.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 4.1.0.dev
78
+ specification_version: 4
79
+ summary: Bundler plugin to show why a package is installed
80
+ test_files: []