diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index 89318045ba28..5390d529b5c1 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -545,6 +545,7 @@ def viz desc: "Open generated gemspec in the specified editor (defaults to $EDITOR or $BUNDLER_EDITOR)" method_option :ext, type: :string, desc: "Generate the boilerplate for C extension code.", enum: EXTENSIONS method_option :git, type: :boolean, default: true, desc: "Initialize a git repo inside your library." + method_option :zeitwerk, type: :boolean, desc: "Configure Zeitwerk as the class loader. Set a default with `bundle config set --global gem.zeitwerk true`." method_option :mit, type: :boolean, desc: "Generate an MIT license file. Set a default with `bundle config set --global gem.mit true`." 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`." method_option :changelog, type: :boolean, desc: "Generate changelog file. Set a default with `bundle config set --global gem.changelog true`." diff --git a/bundler/lib/bundler/cli/gem.rb b/bundler/lib/bundler/cli/gem.rb index 22bcf0e47aa2..d7189413c073 100644 --- a/bundler/lib/bundler/cli/gem.rb +++ b/bundler/lib/bundler/cli/gem.rb @@ -145,6 +145,12 @@ def run config[:ci_config_path] = ".circleci " end + if ask_and_set(:zeitwerk, "Do you want to use Zeitwerk to load classes?", + "With Zeitwerk (https://github.com/fxn/zeitwerk), Ruby can load classes automatically " \ + "based on name conventions so that you don't have to require files manually.") + config[:zeitwerk] = true + end + if ask_and_set(:mit, "Do you want to license your code permissively under the MIT license?", "This means that any other developer or company will be legally allowed to use your code " \ "for free as long as they admit you created it. You can read more about the MIT license " \ diff --git a/bundler/lib/bundler/templates/newgem/README.md.tt b/bundler/lib/bundler/templates/newgem/README.md.tt index f9c97d5c7e56..b9d5b558656b 100644 --- a/bundler/lib/bundler/templates/newgem/README.md.tt +++ b/bundler/lib/bundler/templates/newgem/README.md.tt @@ -19,6 +19,12 @@ If bundler is not being used to manage dependencies, install the gem by executin ```bash gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG ``` +<%- if config[:zeitwerk] -%> + +### Zeitwerk and class loading + +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). +<%- end -%> ## Usage diff --git a/bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt b/bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt index caf6e32f4abf..6d682c14318b 100644 --- a/bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt +++ b/bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt @@ -1,9 +1,25 @@ # frozen_string_literal: true +<%- unless config[:zeitwerk] -%> require_relative "<%= File.basename(config[:namespaced_path]) %>/version" +<%- end -%> <%- if config[:ext] -%> require_relative "<%= File.basename(config[:namespaced_path]) %>/<%= config[:underscored_name] %>" <%- end -%> +<%- if config[:zeitwerk] -%> +require "zeitwerk" +<%- if config[:name].include?("-") -%> +loader = Zeitwerk::Loader.for_gem_extension(<%= config[:constant_array][0..-2].join("::") %>) +<%- else -%> +loader = Zeitwerk::Loader.for_gem +<%- end -%> +loader.setup + +# Client code may eager load the gem, make sure that works. +# If some files or directories should never be eager loaded, +# please configure eager load exceptions in the loader. +loader.eager_load if ENV.key?('CI') +<%- end -%> <%- config[:constant_array].each_with_index do |c, i| -%> <%= " " * i %>module <%= c %> diff --git a/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt b/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt index ced300f37911..b3a3a7ee8bca 100644 --- a/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt +++ b/bundler/lib/bundler/templates/newgem/newgem.gemspec.tt @@ -46,6 +46,9 @@ Gem::Specification.new do |spec| <%- if config[:ext] == 'rust' -%> spec.add_dependency "rb_sys", "~> 0.9.91" <%- end -%> +<%- if config[:zeitwerk] -%> + spec.add_dependency "zeitwerk" +<%- end -%> # For more information and examples about making a new gem, check out our # guide at: https://bundler.io/guides/creating_gem.html diff --git a/bundler/spec/bundler/gem_helper_spec.rb b/bundler/spec/bundler/gem_helper_spec.rb index 94f66537d3eb..9934c74898fb 100644 --- a/bundler/spec/bundler/gem_helper_spec.rb +++ b/bundler/spec/bundler/gem_helper_spec.rb @@ -10,7 +10,7 @@ before(:each) do global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false", "BUNDLE_GEM__LINTER" => "false", - "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false" + "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__ZEITWERK" => "false", "BUNDLE_GEM__CHANGELOG" => "false" git("config --global init.defaultBranch main") bundle "gem #{app_name}" prepare_gemspec(app_gemspec_path) diff --git a/bundler/spec/commands/newgem_spec.rb b/bundler/spec/commands/newgem_spec.rb index 9a5bc9c24f05..8773d54c2bf1 100644 --- a/bundler/spec/commands/newgem_spec.rb +++ b/bundler/spec/commands/newgem_spec.rb @@ -38,7 +38,7 @@ def bundle_exec_standardrb git("config --global github.user bundleuser") global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false", "BUNDLE_GEM__LINTER" => "false", - "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false" + "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__ZEITWERK" => "false", "BUNDLE_GEM__CHANGELOG" => "false" end describe "git repo initialization" do @@ -74,6 +74,36 @@ def bundle_exec_standardrb end end + shared_examples_for "--zeitwerk flag" do + let(:gem_name) { "my_gem" } + + before do + bundle "gem #{gem_name} --zeitwerk" + end + it "configures zeitwerk" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "zeitwerk"') + expect(bundled_app("#{gem_name}/README.md").read).to include("## Zeitwerk") + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to include <<~RUBY + require "zeitwerk" + loader = Zeitwerk::Loader.for_gem + loader.setup + RUBY + end + end + + shared_examples_for "--no-zeitwerk flag" do + before do + bundle "gem #{gem_name} --no-zeitwerk" + end + it "does not configure zeitwerk" do + gem_skeleton_assertions + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to_not include('spec.add_dependency "zeitwerk"') + expect(bundled_app("#{gem_name}/README.md").read).to_not include("## Zeitwerk") + expect(bundled_app("#{gem_name}/lib/#{require_path}.rb").read).to_not include('require "zeitwerk"') + end + end + shared_examples_for "--mit flag" do before do bundle "gem #{gem_name} --mit" @@ -1408,6 +1438,28 @@ def create_temporary_dir(dir) end end + context "testing --zeitwerk option against bundle config settings" do + let(:gem_name) { "my_gem" } + + let(:require_path) { "my_gem" } + + context "with zeitwerk option in bundle config settings set to true" do + before do + global_config "BUNDLE_GEM__ZEITWERK" => "true" + end + it_behaves_like "--zeitwerk flag" + it_behaves_like "--no-zeitwerk flag" + end + + context "with zeitwerk option in bundle config settings set to false" do + before do + global_config "BUNDLE_GEM__ZEITWERK" => "false" + end + it_behaves_like "--zeitwerk flag" + it_behaves_like "--no-zeitwerk flag" + end + end + context "testing --github-username option against git and bundle config settings" do context "without git config set" do before do @@ -1716,6 +1768,31 @@ def create_temporary_dir(dir) expect(bundled_app("foobar/.github/workflows/main.yml")).to exist end + it "asks about Zeitwerk" do + global_config "BUNDLE_GEM__ZEITWERK" => nil + + bundle "gem foobar" do |input, _, _| + input.puts "yes" + end + + expect(bundled_app("foobar/foobar.gemspec").read).to include('spec.add_dependency "zeitwerk"') + end + + context("gem extensions") do + let(:gem_name) { "my-gem" } + + it "configures zeitwerk detecting the gem extension" do + bundle "gem my-gem --zeitwerk" + + expect(bundled_app("#{gem_name}/#{gem_name}.gemspec").read).to include('spec.add_dependency "zeitwerk"') + expect(bundled_app("#{gem_name}/lib/my/gem.rb").read).to include <<~RUBY + require "zeitwerk" + loader = Zeitwerk::Loader.for_gem_extension(My) + loader.setup + RUBY + end + end + it "asks about MIT license" do global_config "BUNDLE_GEM__MIT" => nil diff --git a/bundler/spec/other/major_deprecation_spec.rb b/bundler/spec/other/major_deprecation_spec.rb index 036c855c4e3f..e135f0b87036 100644 --- a/bundler/spec/other/major_deprecation_spec.rb +++ b/bundler/spec/other/major_deprecation_spec.rb @@ -608,7 +608,7 @@ describe "deprecating rubocop" do before do global_config "BUNDLE_GEM__MIT" => "false", "BUNDLE_GEM__TEST" => "false", "BUNDLE_GEM__COC" => "false", - "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false" + "BUNDLE_GEM__ZEITWERK" => "false", "BUNDLE_GEM__CI" => "false", "BUNDLE_GEM__CHANGELOG" => "false" end context "bundle gem --rubocop" do