From 95baf46123e19eb555eb9aabb9ec2aa5618318ee Mon Sep 17 00:00:00 2001 From: Patrick Hogan Date: Tue, 3 Sep 2024 12:39:45 -0500 Subject: [PATCH] Fixes support for generators as well as manifest and install tasks when working in a Rails engine. app:stimulus:* tasks correctly run in test/dummy and put files in the right places. stimulus:* tasks exist and puts files under the engine namespace. The stimulus controller generator, when run in the engine root, includes the engine namespace. Namespaces are correctly handled in index.js and in terms of where files are placed. --- lib/generators/stimulus/stimulus_generator.rb | 15 ++- .../controllers/hello_controller.js | 7 -- .../javascript/controllers/index_for_node.js | 8 -- lib/install/stimulus_with_bun.rb | 33 ++++--- lib/install/stimulus_with_importmap.rb | 41 ++++---- lib/install/stimulus_with_node.rb | 33 +++---- lib/rake/extensions.rb | 23 +++++ lib/tasks/stimulus_tasks.rake | 99 +++++++++++++++---- test/generator_test.rb | 5 +- 9 files changed, 171 insertions(+), 93 deletions(-) delete mode 100644 lib/install/app/javascript/controllers/hello_controller.js delete mode 100644 lib/install/app/javascript/controllers/index_for_node.js create mode 100644 lib/rake/extensions.rb diff --git a/lib/generators/stimulus/stimulus_generator.rb b/lib/generators/stimulus/stimulus_generator.rb index 5af2437..ccf71a5 100644 --- a/lib/generators/stimulus/stimulus_generator.rb +++ b/lib/generators/stimulus/stimulus_generator.rb @@ -6,17 +6,22 @@ class StimulusGenerator < Rails::Generators::NamedBase # :nodoc: class_option :skip_manifest, type: :boolean, default: false, desc: "Don't update the stimulus manifest" def copy_view_files - @attribute = stimulus_attribute_value(controller_name) - template "controller.js", "app/javascript/controllers/#{controller_name}_controller.js" + @attribute = stimulus_attribute_value(File.join(class_path, file_name)) + template "controller.js", File.join("app/javascript/controllers", class_path, "#{file_name}_controller.js") rails_command "stimulus:manifest:update" unless Rails.root.join("config/importmap.rb").exist? || options[:skip_manifest] end private - def controller_name - name.underscore.gsub(/_controller$/, "") + + def file_name + @_file_name ||= remove_possible_suffix(super) + end + + def remove_possible_suffix(name) + name.sub(/_?controller$/i, "") end def stimulus_attribute_value(controller_name) - controller_name.gsub(/\//, "--").gsub("_", "-") + controller_name.sub(/\A\/+/, "").sub(/\/+\z/, "").gsub(/\//, "--").gsub("_", "-") end end diff --git a/lib/install/app/javascript/controllers/hello_controller.js b/lib/install/app/javascript/controllers/hello_controller.js deleted file mode 100644 index 5975c07..0000000 --- a/lib/install/app/javascript/controllers/hello_controller.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - connect() { - this.element.textContent = "Hello World!" - } -} diff --git a/lib/install/app/javascript/controllers/index_for_node.js b/lib/install/app/javascript/controllers/index_for_node.js deleted file mode 100644 index d0685d3..0000000 --- a/lib/install/app/javascript/controllers/index_for_node.js +++ /dev/null @@ -1,8 +0,0 @@ -// This file is auto-generated by ./bin/rails stimulus:manifest:update -// Run that command whenever you add a new controller or create them with -// ./bin/rails generate stimulus controllerName - -import { application } from "./application" - -import HelloController from "./hello_controller" -application.register("hello", HelloController) diff --git a/lib/install/stimulus_with_bun.rb b/lib/install/stimulus_with_bun.rb index e6250b8..e716530 100644 --- a/lib/install/stimulus_with_bun.rb +++ b/lib/install/stimulus_with_bun.rb @@ -1,18 +1,19 @@ -say "Create controllers directory" -empty_directory "app/javascript/controllers" -copy_file "#{__dir__}/app/javascript/controllers/index_for_node.js", - "app/javascript/controllers/index.js" -copy_file "#{__dir__}/app/javascript/controllers/application.js", - "app/javascript/controllers/application.js" -copy_file "#{__dir__}/app/javascript/controllers/hello_controller.js", - "app/javascript/controllers/hello_controller.js" +say "Installing Stimulus (for bun) into #{destination_root}" +inside destination_root do + say "Create controllers directory" + empty_directory "app/javascript/controllers" + copy_file "#{__dir__}/app/javascript/controllers/application.js", "app/javascript/controllers/application.js" + run "rails generate stimulus hello --without-manifest" + run "rails stimulus:manifest:update" -if (Rails.root.join("app/javascript/application.js")).exist? - say "Import Stimulus controllers" - append_to_file "app/javascript/application.js", %(import "./controllers"\n) -else - say %(Couldn't find "app/javascript/application.js".\nYou must import "./controllers" in your JavaScript entrypoint file), :red -end + if File.exist?("app/javascript/application.js") + say "Import Stimulus controllers" + append_to_file "app/javascript/application.js", %(import "./controllers"\n) + else + say %(Couldn't find "app/javascript/application.js".), :red + say %(You must import "./controllers" in your JavaScript entrypoint file), :red + end -say "Install Stimulus" -run "bun add @hotwired/stimulus" + say "Install Stimulus" + run "bun add @hotwired/stimulus" +end diff --git a/lib/install/stimulus_with_importmap.rb b/lib/install/stimulus_with_importmap.rb index 170ee38..886da54 100644 --- a/lib/install/stimulus_with_importmap.rb +++ b/lib/install/stimulus_with_importmap.rb @@ -1,22 +1,27 @@ -say "Create controllers directory" -empty_directory "app/javascript/controllers" -copy_file "#{__dir__}/app/javascript/controllers/index_for_importmap.js", - "app/javascript/controllers/index.js" -copy_file "#{__dir__}/app/javascript/controllers/application.js", - "app/javascript/controllers/application.js" -copy_file "#{__dir__}/app/javascript/controllers/hello_controller.js", - "app/javascript/controllers/hello_controller.js" +say "Installing Stimulus (for importmap-rails) into #{destination_root}" +inside destination_root do + say "Create controllers directory" + empty_directory "app/javascript/controllers" + copy_file "#{__dir__}/app/javascript/controllers/index_for_importmap.js", "app/javascript/controllers/index.js" + copy_file "#{__dir__}/app/javascript/controllers/application.js", "app/javascript/controllers/application.js" + run "rails generate stimulus hello --without-manifest" -say "Import Stimulus controllers" -append_to_file "app/javascript/application.js", %(import "controllers"\n) + if File.exist?("app/javascript/application.js") + say "Import Stimulus controllers" + append_to_file "app/javascript/application.js", %(import "controllers"\n) + else + say %(Couldn't find "app/javascript/application.js".), :red + say %(You must import "controllers" in your JavaScript entrypoint file), :red + end -say "Pin Stimulus" -say %(Appending: pin "@hotwired/stimulus", to: "stimulus.min.js") -append_to_file "config/importmap.rb", %(pin "@hotwired/stimulus", to: "stimulus.min.js"\n) + say "Pin Stimulus" + say %(Appending: pin "@hotwired/stimulus", to: "stimulus.min.js") + append_to_file "config/importmap.rb", %(pin "@hotwired/stimulus", to: "stimulus.min.js"\n) -say %(Appending: pin "@hotwired/stimulus-loading", to: "stimulus-loading.js") -append_to_file "config/importmap.rb", %(pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"\n) + say %(Appending: pin "@hotwired/stimulus-loading", to: "stimulus-loading.js") + append_to_file "config/importmap.rb", %(pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"\n) -say "Pin all controllers" -say %(Appending: pin_all_from "app/javascript/controllers", under: "controllers") -append_to_file "config/importmap.rb", %(pin_all_from "app/javascript/controllers", under: "controllers"\n) + say "Pin all controllers" + say %(Appending: pin_all_from "#{javascript_root}/controllers", under: "controllers") + append_to_file "config/importmap.rb", %(pin_all_from "#{javascript_root}/controllers", under: "controllers"\n) +end diff --git a/lib/install/stimulus_with_node.rb b/lib/install/stimulus_with_node.rb index 50431dc..e7bac13 100644 --- a/lib/install/stimulus_with_node.rb +++ b/lib/install/stimulus_with_node.rb @@ -1,22 +1,19 @@ -say "Create controllers directory" -empty_directory "app/javascript/controllers" -copy_file "#{__dir__}/app/javascript/controllers/index_for_node.js", - "app/javascript/controllers/index.js" -copy_file "#{__dir__}/app/javascript/controllers/application.js", - "app/javascript/controllers/application.js" -copy_file "#{__dir__}/app/javascript/controllers/hello_controller.js", - "app/javascript/controllers/hello_controller.js" +say "Installing Stimulus (for node) into #{destination_root}" +inside destination_root do + say "Create controllers directory" + empty_directory "app/javascript/controllers" + copy_file "#{__dir__}/app/javascript/controllers/application.js", "app/javascript/controllers/application.js" + run "rails generate stimulus hello --without-manifest" + run "rails stimulus:manifest:update" -if (Rails.root.join("app/javascript/application.js")).exist? - say "Import Stimulus controllers" - append_to_file "app/javascript/application.js", %(import "./controllers"\n) -else - say %(Couldn't find "app/javascript/application.js".\nYou must import "./controllers" in your JavaScript entrypoint file), :red -end + if File.exist?("app/javascript/application.js") + say "Import Stimulus controllers" + append_to_file "app/javascript/application.js", %(import "./controllers"\n) + else + say %(Couldn't find "app/javascript/application.js".), :red + say %(You must import "./controllers" in your JavaScript entrypoint file), :red + end -say "Install Stimulus" -if (Rails.root.join("bun.config.js")).exist? - run "bun add @hotwired/stimulus" -else + say "Install Stimulus" run "yarn add @hotwired/stimulus" end diff --git a/lib/rake/extensions.rb b/lib/rake/extensions.rb new file mode 100644 index 0000000..ad643fd --- /dev/null +++ b/lib/rake/extensions.rb @@ -0,0 +1,23 @@ +# Extend Rake to allow unscoping from within a scope/namespace. +# This allows engine tasks to unscope from :app and define top-level tasks. +module Rake + + module TaskManager + def unscoped + current_scope = @scope + @scope = Scope.make + ns = NameSpace.new(self, @scope) + yield(ns) + ns + ensure + @scope = current_scope + end + end + + module DSL + def unscoped(&block) + Rake.application.unscoped(&block) + end + end + +end \ No newline at end of file diff --git a/lib/tasks/stimulus_tasks.rake b/lib/tasks/stimulus_tasks.rake index ec08183..ae5e247 100644 --- a/lib/tasks/stimulus_tasks.rake +++ b/lib/tasks/stimulus_tasks.rake @@ -1,46 +1,55 @@ require "stimulus/manifest" +require "rake/extensions" +require "rails/generators" +require "rails/generators/rails/app/app_generator" module Stimulus module Tasks extend self - def run_stimulus_install_template(path) - system RbConfig.ruby, "./bin/rails", "app:template", "LOCATION=#{File.expand_path("../install/#{path}.rb", __dir__)}" + + def detect_install_template(root) + if root.join("config/importmap.rb").exist? + "importmap" + elsif root.join("package.json").exist? + if root.join("bun.config.js").exist? + "bun" + else + "node" + end + end end - def using_bun? - Rails.root.join("bun.config.js").exist? + def self.run_stimulus_install_template(path, root) + Rails::Generators::AppGenerator.apply_rails_template(File.expand_path("../install/#{path}.rb", __dir__), root) end + end end namespace :stimulus do desc "Install Stimulus into the app" task :install do - if Rails.root.join("config/importmap.rb").exist? - Rake::Task["stimulus:install:importmap"].invoke - elsif Rails.root.join("package.json").exist? && Stimulus::Tasks.using_bun? - Rake::Task["stimulus:install:bun"].invoke - elsif Rails.root.join("package.json").exist? - Rake::Task["stimulus:install:node"].invoke + if (install_template = Stimulus::Tasks.detect_install_template(Rails.root)) + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_#{install_template}", Rails.root) else puts "You must either be running with node (package.json) or importmap-rails (config/importmap.rb) to use this gem." end end namespace :install do - desc "Install Stimulus on an app running importmap-rails" + desc "Install Stimulus in an app running importmap-rails" task :importmap do - Stimulus::Tasks.run_stimulus_install_template "stimulus_with_importmap" + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_importmap", Rails.root) end - desc "Install Stimulus on an app running node" + desc "Install Stimulus in an app running node" task :node do - Stimulus::Tasks.run_stimulus_install_template "stimulus_with_node" + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_node", Rails.root) end - desc "Install Stimulus on an app running bun" + desc "Install Stimulus in an app running bun" task :bun do - Stimulus::Tasks.run_stimulus_install_template "stimulus_with_bun" + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_bun", Rails.root) end end @@ -52,8 +61,7 @@ namespace :stimulus do desc "Update the Stimulus manifest (will overwrite controllers/index.js)" task :update do - manifest = - Stimulus::Manifest.generate_from(Rails.root.join("app/javascript/controllers")) + manifest = Stimulus::Manifest.generate_from(Rails.root.join("app/javascript/controllers")) File.open(Rails.root.join("app/javascript/controllers/index.js"), "w+") do |index| index.puts "// This file is auto-generated by ./bin/rails stimulus:manifest:update" @@ -66,3 +74,58 @@ namespace :stimulus do end end end + +if defined?(ENGINE_ROOT) && (engine = Rails::Engine.find(ENGINE_ROOT)) + unscoped do + namespace :stimulus do + + desc "Install Stimulus into the engine" + task :install do + if (install_template = Stimulus::Tasks.detect_install_template(engine.root)) + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_#{install_template}", engine.root) + else + puts "You must either be running with node/bun (package.json) or importmap-rails (config/importmap.rb) to use this gem." + end + end + + namespace :install do + desc "Install Stimulus in an engine running importmap-rails" + task :importmap do + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_importmap", Rails.root) + end + + desc "Install Stimulus in an engine running node" + task :node do + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_node", Rails.root) + end + + desc "Install Stimulus in an engine running bun" + task :bun do + Stimulus::Tasks.run_stimulus_install_template("stimulus_with_bun", Rails.root) + end + end + + namespace :manifest do + + desc "Show the current engine Stimulus manifest (all installed controllers)" + task :display do + puts Stimulus::Manifest.generate_from(engine.root.join("app/javascript/controllers")) + end + + desc "Update the engine Stimulus manifest (will overwrite controllers/index.js)" + task :update do + manifest = Stimulus::Manifest.generate_from(engine.root.join("app/javascript/controllers")) + File.open(engine.root.join("app/javascript/controllers/index.js"), "w+") do |index| + index.puts "// This file is auto-generated by ./bin/rails stimulus:manifest:update" + index.puts "// Run that command whenever you add a new controller or create them with" + index.puts "// ./bin/rails generate stimulus controllerName" + index.puts + index.puts %(import { application } from "./application") + index.puts manifest + end + end + + end + end + end +end diff --git a/test/generator_test.rb b/test/generator_test.rb index df39713..df5e9a9 100644 --- a/test/generator_test.rb +++ b/test/generator_test.rb @@ -26,7 +26,6 @@ class StimulusGeneratorTest < Rails::Generators::TestCase assert_file "app/javascript/controllers/hello_world_controller.js", /data-controller="hello-world"/ end - test "generating with camelized name and lower case first letter" do run_generator ["helloWorld"] @@ -46,9 +45,9 @@ class StimulusGeneratorTest < Rails::Generators::TestCase end test "generating with namespaced name" do - run_generator ["hello/world"] + run_generator ["hello/happy/world"] - assert_file "app/javascript/controllers/hello/world_controller.js", /data-controller="hello--world"/ + assert_file "app/javascript/controllers/hello/happy/world_controller.js", /data-controller="hello--happy--world"/ end test "generating with namespaced and camelized name" do