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
1 change: 1 addition & 0 deletions bundler/lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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`."
Expand Down
6 changes: 6 additions & 0 deletions bundler/lib/bundler/cli/gem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

@jeromedalbert jeromedalbert Nov 1, 2024

Choose a reason for hiding this comment

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

Should we ask for this, which would set a BUNDLE_GEM__ZEITWERK config setting that will be remembered across bundle gem calls?

Maybe :zeitwerk is one of those options like :exe that could be left as one-off options. While one may want zeitwerk sometimes (or often), I don't know if it is a setting that should be remembered as if you are doing a very simple gem with 2-3 files, it's easy enough to require manually and one might want to forego zeitwerk to avoid a third-party dependency in their gem. But because they could have enabled zeitwerk before, zeitwerk would be enabled for their new small gem too. I guess in this case one could always provide --no-zeitwerk, so maybe this is fine as-is.

Just thought I would comment in case anyone has opinions on this.

Copy link
Author

Choose a reason for hiding this comment

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

@jeromedalbert I went with the same approach it uses for the CoC or the MIT license, which results in question + --zeitwerk/--no-zeitwerk option + memorized answer in ~/.bundle/config. I personally think it's fine and consistent but happy to change it if you want me to.


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 " \
Expand Down
6 changes: 6 additions & 0 deletions bundler/lib/bundler/templates/newgem/README.md.tt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions bundler/lib/bundler/templates/newgem/lib/newgem.rb.tt
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

I always wondered why the local var. Any drawbacks in doing

Suggested change
loader = Zeitwerk::Loader.for_gem
Zeitwerk::Loader.for_gem.setup

<%- 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 %>
Expand Down
3 changes: 3 additions & 0 deletions bundler/lib/bundler/templates/newgem/newgem.gemspec.tt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bundler/spec/bundler/gem_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
79 changes: 78 additions & 1 deletion bundler/spec/commands/newgem_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion bundler/spec/other/major_deprecation_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down