Skip to content

Commit b8aab45

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 b8aab45

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 uses [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 running this gem in a context 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)