diff --git a/lib/hanami/cli/commands/app/generate/command.rb b/lib/hanami/cli/commands/app/generate/command.rb index d98ba53e..6ba5ac9d 100644 --- a/lib/hanami/cli/commands/app/generate/command.rb +++ b/lib/hanami/cli/commands/app/generate/command.rb @@ -37,24 +37,58 @@ def generator_class # @since 2.2.0 # @api private def call(name:, slice: nil, **opts) - if slice - base_path = fs.join("slices", inflector.underscore(slice)) - raise MissingSliceError.new(slice) unless fs.exist?(base_path) + slice ||= detect_slice_from_cwd - generator.call( - key: name, - namespace: slice, - base_path: base_path, - **opts, - ) - else + if slice.nil? generator.call( key: name, namespace: app.namespace, base_path: "app", **opts, ) + return end + + slice_root = slice.respond_to?(:root) ? slice.root : detect_slice_root(slice) + raise MissingSliceError.new(slice) unless fs.exist?(slice_root) + + generator.call( + key: name, + namespace: slice, + base_path: slice_root, + **opts, + ) + end + + private + + def detect_slice_from_cwd + slices_by_root = app.slices.with_nested.each.to_h { |slice| [slice.root.to_s, slice] } + slices_by_root[fs.pwd] + end + + # Returns the root for the given slice name. + # + # This currently works with top-level slices only, and it simply appends the slice's + # name onto the "slices/" dir, returning e.g. "slices/main" when given "main". + # + # TODO: Make this work with nested slices when given slash-delimited slice names like + # "parent/child", which should look for "slices/parent/slices/child". + # + # This method makes two checks for the slice root (building off both `app.root` as well + # as `fs`). This is entirely to account for how we test commands, with most tests using + # an in-memory `fs` adapter, any files created via which will be invisible to the `app`, + # which doesn't know about the `fs`. + # + # FIXME: It would be better to find a way for this to make one check only. An ideal + # approach would be to use the slice_name to find actual slice registered within + # `app.slices`. To do this, we'd probably need to stop testing with an in-memory `fs` + # here. + def detect_slice_root(slice_name) + slice_root_in_fs = fs.join("slices", inflector.underscore(slice_name)) + return slice_root_in_fs if fs.exist?(slice_root_in_fs) + + app.root.join("slices", inflector.underscore(slice_name)) end end end diff --git a/spec/unit/hanami/cli/commands/app/generate/slice_detection_spec.rb b/spec/unit/hanami/cli/commands/app/generate/slice_detection_spec.rb new file mode 100644 index 00000000..2225e98d --- /dev/null +++ b/spec/unit/hanami/cli/commands/app/generate/slice_detection_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require "hanami" + +RSpec.describe "slice detection", :app_integration do + subject(:cmd) { Hanami::CLI::Commands::App::Generate::View.new(inflector: inflector, out: out) } + + let(:inflector) { Dry::Inflector.new } + let(:out) { StringIO.new } + def output + out.rewind && out.read.chomp + end + + before do + with_directory(@app_dir) do + write "config/app.rb", <<~RUBY + module TestApp + class App < Hanami::App + end + end + RUBY + + write "slices/main/.keep", "" + write "slices/admin/.keep", "" + write "slices/admin/slices/search/.keep", "" + + require "hanami/prepare" + end + end + + around do |example| + @app_dir = make_tmp_directory + orig_cwd = Dir.pwd + + # Use non-block form of chdir to avoid "conflicting chdir" warnings when we do it inside the + # examples + Dir.chdir(@app_dir) + example.call + Dir.chdir(orig_cwd) + end + + describe "slice detection from current working directory" do + it "detects the slice based on the current working directory" do + Dir.chdir("slices/main") do + subject.call(name: "showcase") + end + + expect(File.exist?("slices/main/views/showcase.rb")).to be true + end + + context "with deeply nested slices existing" do + it "determines the nested slice when inside of it" do + Dir.chdir("slices/admin/slices/search") do + subject.call(name: "panel") + end + + expect(File.exist?("slices/admin/slices/search/views/panel.rb")).to be true + end + + it "still determines the parent slice when inside of it" do + Dir.chdir("slices/main") do + subject.call(name: "bookcase") + end + + expect(File.exist?("slices/main/views/bookcase.rb")).to be true + end + end + end + + context "when --slice option is provided" do + it "in child slice - respects the --slice option - overwrites slice detection" do + Dir.chdir("slices/admin/slices/search") do + subject.call(name: "bookcase", slice: "main") + end + + expect(File.exist?("slices/main/views/bookcase.rb")).to be true + end + + it "in sibling slice - respects the --slice option - overwrites slice detection" do + Dir.chdir("slices/main") do + subject.call(name: "shop", slice: "admin") + end + + expect(File.exist?("slices/admin/views/shop.rb")).to be true + end + end + + context "when working outside slice directory" do + it "uses app namespace when not in a slice directory" do + subject.call(name: "important_view") + + expect(File.exist?("app/views/important_view.rb")).to be true + end + end +end