Skip to content

Commit 43fa181

Browse files
committed
Add option to configure Zeitwerk in new gems
Add a question to configure Zeitwerk when creating new gems (false by default). It also supports a --zeitwerk flag to do it: bundle gem mygem --zeitwerk
1 parent c46bf73 commit 43fa181

File tree

8 files changed

+112
-3
lines changed

8 files changed

+112
-3
lines changed

bundler/lib/bundler/cli.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,7 @@ def viz
545545
desc: "Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)"
546546
method_option :ext, type: :string, desc: "Generate the boilerplate for C extension code.", enum: EXTENSIONS
547547
method_option :git, type: :boolean, default: true, desc: "Initialize a git repo inside your library."
548+
method_option :zeitwerk, type: :boolean, desc: "Configure Zeitwerk as the class loader. Set a default with `bundle config set --global gem.zeitwerk true`."
548549
method_option :mit, type: :boolean, desc: "Generate an MIT license file. Set a default with `bundle config set --global gem.mit true`."
549550
method_option :rubocop, type: :boolean, desc: "Add rubocop to the generated Rakefile and gemspec. Set a default with `bundle config set --global gem.rubocop true`."
550551
method_option :changelog, type: :boolean, desc: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`."

bundler/lib/bundler/cli/gem.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ def run
145145
config[:ci_config_path] = ".circleci "
146146
end
147147

148+
if ask_and_set(:zeitwerk, "Do you want to use Zeitwerk to load classes?",
149+
"With Zeitwerk (https://github.com/fxn/zeitwerk), Ruby can load classes automatically " \
150+
"based on name conventions so that you don't have to require files manually.")
151+
config[:zeitwerk] = true
152+
end
153+
148154
if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?",
149155
"This means that any other developer or company will be legally allowed to use your code " \
150156
"for free as long as they admit you created it. You can read more about the MIT license " \

bundler/lib/bundler/templates/newgem/README.md.tt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ If bundler is not being used to manage dependencies, install the gem by executin
1919
```bash
2020
gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
2121
```
22+
<%- if config[:zeitwerk] -%>
23+
24+
### Zeitwerk and class loading
25+
26+
This gem is configured to use [Zeitwerk](https://github.com/fxn/zeitwerk), which, by default, loads classes lazily as they are referenced in the code. In production environments, it's common to load code eagerly for performance reasons. If you're using this gem in an execution environment that supports Zeitwerk—such as Rails or Hanami—no additional configuration is necessary. Otherwise, you may want to [eager load this gem](https://github.com/fxn/zeitwerk?tab=readme-ov-file#eager-loading).
27+
<%- end -%>
2228

2329
## Usage
2430

bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
# frozen_string_literal: true
22

3+
<%- unless config[:zeitwerk] -%>
34
require_relative "<%= File.basename(config[:namespaced_path]) %>/version"
5+
<%- end -%>
46
<%- if config[:ext] -%>
57
require_relative "<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>"
68
<%- end -%>
9+
<%- if config[:zeitwerk] -%>
10+
require "zeitwerk"
11+
<%- if config[:name].include?("-") -%>
12+
loader = Zeitwerk::Loader.for_gem_extension(<%= config[:constant_array][0..-2].join("::") %>)
13+
<%- else -%>
14+
loader = Zeitwerk::Loader.for_gem
15+
<%- end -%>
16+
loader.setup
17+
18+
# Client code may eager load the gem, make sure that works.
19+
# If some files or directories should never be eager loaded,
20+
# please configure eager load exceptions in the loader.
21+
loader.eager_load if ENV.key?('CI')
22+
<%- end -%>
723

824
<%- config[:constant_array].each_with_index do |c, i| -%>
925
<%= " " * i %>module <%= c %>

bundler/lib/bundler/templates/newgem/newgem.gemspec.tt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ Gem::Specification.new do |spec|
4646
<%- if config[:ext] == 'rust' -%>
4747
spec.add_dependency "rb_sys", "~> 0.9.91"
4848
<%- end -%>
49+
<%- if config[:zeitwerk] -%>
50+
spec.add_dependency "zeitwerk"
51+
<%- end -%>
4952

5053
# For more information and examples about making a new gem, check out our
5154
# guide at: https://bundler.io/guides/creating_gem.html

bundler/spec/bundler/gem_helper_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
before(:each) do
1212
global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false", "BUNDLE_GEM__LINTER" => "false",
13-
"BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false"
13+
"BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__ZEITWERK" => "false", "BUNDLE_GEM__CHANGELOG" => "false"
1414
git("config --global init.defaultBranch main")
1515
bundle "gem #{app_name}"
1616
prepare_gemspec(app_gemspec_path)

bundler/spec/commands/newgem_spec.rb

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def bundle_exec_standardrb
3838
git("config --global github.user bundleuser")
3939

4040
global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false", "BUNDLE_GEM__LINTER" => "false",
41-
"BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false"
41+
"BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__ZEITWERK" => "false", "BUNDLE_GEM__CHANGELOG" => "false"
4242
end
4343

4444
describe "git repo initialization" do
@@ -74,6 +74,36 @@ def bundle_exec_standardrb
7474
end
7575
end
7676

77+
shared_examples_for "--zeitwerk flag" do
78+
let(:gem_name) { "my_gem" }
79+
80+
before do
81+
bundle "gem #{gem_name} --zeitwerk"
82+
end
83+
it "configures zeitwerk" do
84+
gem_skeleton_assertions
85+
expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "zeitwerk"')
86+
expect(bundled_app("#{gem_name}/README.md").read).to include("## Zeitwerk")
87+
expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to include <<~RUBY
88+
require "zeitwerk"
89+
loader = Zeitwerk::Loader.for_gem
90+
loader.setup
91+
RUBY
92+
end
93+
end
94+
95+
shared_examples_for "--no-zeitwerk flag" do
96+
before do
97+
bundle "gem #{gem_name} --no-zeitwerk"
98+
end
99+
it "does not configure zeitwerk" do
100+
gem_skeleton_assertions
101+
expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "zeitwerk"')
102+
expect(bundled_app("#{gem_name}/README.md").read).to_not include("## Zeitwerk")
103+
expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to_not include('require "zeitwerk"')
104+
end
105+
end
106+
77107
shared_examples_for "--mit flag" do
78108
before do
79109
bundle "gem #{gem_name} --mit"
@@ -1408,6 +1438,28 @@ def create_temporary_dir(dir)
14081438
end
14091439
end
14101440

1441+
context "testing --zeitwerk option against bundle config settings" do
1442+
let(:gem_name) { "my_gem" }
1443+
1444+
let(:require_path) { "my_gem" }
1445+
1446+
context "with zeitwerk option in bundle config settings set to true" do
1447+
before do
1448+
global_config "BUNDLE_GEM__ZEITWERK" => "true"
1449+
end
1450+
it_behaves_like "--zeitwerk flag"
1451+
it_behaves_like "--no-zeitwerk flag"
1452+
end
1453+
1454+
context "with zeitwerk option in bundle config settings set to false" do
1455+
before do
1456+
global_config "BUNDLE_GEM__ZEITWERK" => "false"
1457+
end
1458+
it_behaves_like "--zeitwerk flag"
1459+
it_behaves_like "--no-zeitwerk flag"
1460+
end
1461+
end
1462+
14111463
context "testing --github-username option against git and bundle config settings" do
14121464
context "without git config set" do
14131465
before do
@@ -1716,6 +1768,31 @@ def create_temporary_dir(dir)
17161768
expect(bundled_app("foobar/.github/workflows/main.yml")).to exist
17171769
end
17181770

1771+
it "asks about Zeitwerk" do
1772+
global_config "BUNDLE_GEM__ZEITWERK" => nil
1773+
1774+
bundle "gem foobar" do |input, _, _|
1775+
input.puts "yes"
1776+
end
1777+
1778+
expect(bundled_app("foobar/foobar.gemspec").read).to include('spec.add_dependency "zeitwerk"')
1779+
end
1780+
1781+
context("gem extensions") do
1782+
let(:gem_name) { "my-gem" }
1783+
1784+
it "configures zeitwerk detecting the gem extension" do
1785+
bundle "gem my-gem --zeitwerk"
1786+
1787+
expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "zeitwerk"')
1788+
expect(bundled_app("#{gem_name}/lib/my/gem.rb").read).to include <<~RUBY
1789+
require "zeitwerk"
1790+
loader = Zeitwerk::Loader.for_gem_extension(My)
1791+
loader.setup
1792+
RUBY
1793+
end
1794+
end
1795+
17191796
it "asks about MIT license" do
17201797
global_config "BUNDLE_GEM__MIT" => nil
17211798

bundler/spec/other/major_deprecation_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,7 @@
608608
describe "deprecating rubocop" do
609609
before do
610610
global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false",
611-
"BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false"
611+
"BUNDLE_GEM__ZEITWERK" => "false", "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false"
612612
end
613613

614614
context "bundle gem --rubocop" do

0 commit comments

Comments
 (0)