diff --git a/Gemfile.lock b/Gemfile.lock index db7ed12a01..37737c688a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -112,7 +112,7 @@ PATH railties (7.2.0.alpha) actionpack (= 7.2.0.alpha) activesupport (= 7.2.0.alpha) - irb + irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) @@ -288,10 +288,10 @@ GEM actionpack (>= 6.0.0) activesupport (>= 6.0.0) railties (>= 6.0.0) - io-console (0.7.1) - irb (1.11.0) - rdoc - reline (>= 0.3.8) + io-console (0.7.2) + irb (1.13.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) @@ -415,7 +415,7 @@ GEM rb-inotify (0.10.1) ffi (~> 1.0) rbtree (0.4.6) - rdoc (6.6.2) + rdoc (6.6.3.1) psych (>= 4.0.0) redcarpet (3.2.3) redis (5.0.8) @@ -425,7 +425,7 @@ GEM redis-namespace (1.11.0) redis (>= 4) regexp_parser (2.8.3) - reline (0.4.1) + reline (0.5.4) io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index bdd577548a..b80dee365b 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,14 @@ +* Implement Rails console commands and helpers with IRB v1.13's extension APIs + + Rails console users will now see `helper`, `controller`, `new_session`, and `app` under + IRB help message's `Helper methods` category. And `reload!` command will be displayed under + the new `Rails console` commands category. + + Prior to this change, Rails console's commands and helper methods are added through IRB's + private components and don't show up in its help message, which led to poor discoverability. + + *Stan Lo* + * Remove deprecated `Rails::Generators::Testing::Behaviour`. *Rafael Mendonça França* diff --git a/railties/lib/rails/commands/console/console_command.rb b/railties/lib/rails/commands/console/console_command.rb index 41e25074a5..e8d48d6d28 100644 --- a/railties/lib/rails/commands/console/console_command.rb +++ b/railties/lib/rails/commands/console/console_command.rb @@ -4,66 +4,6 @@ module Rails class Console - module BacktraceCleaner - def filter_backtrace(bt) - if result = super - Rails.backtrace_cleaner.filter([result]).first - end - end - end - - class IRBConsole - def initialize(app) - @app = app - - require "irb" - require "irb/completion" - - IRB::WorkSpace.prepend(BacktraceCleaner) - IRB::ExtendCommandBundle.include(Rails::ConsoleMethods) - end - - def name - "IRB" - end - - def start - IRB.setup(nil) - - if !Rails.env.local? && !ENV.key?("IRB_USE_AUTOCOMPLETE") - IRB.conf[:USE_AUTOCOMPLETE] = false - end - - env = colorized_env - app_name = @app.class.module_parent_name.underscore.dasherize - prompt_prefix = "#{app_name}(#{env})" - - IRB.conf[:PROMPT][:RAILS_PROMPT] = { - PROMPT_I: "#{prompt_prefix}> ", - PROMPT_S: "#{prompt_prefix}%l ", - PROMPT_C: "#{prompt_prefix}* ", - RETURN: "=> %s\n" - } - - # Respect user's choice of prompt mode. - IRB.conf[:PROMPT_MODE] = :RAILS_PROMPT if IRB.conf[:PROMPT_MODE] == :DEFAULT - IRB::Irb.new.run(IRB.conf) - end - - def colorized_env - case Rails.env - when "development" - IRB::Color.colorize("dev", [:BLUE]) - when "test" - IRB::Color.colorize("test", [:BLUE]) - when "production" - IRB::Color.colorize("prod", [:RED]) - else - Rails.env - end - end - end - def self.start(*args) new(*args).start end @@ -83,7 +23,10 @@ def initialize(app, options = {}) app.load_console - @console = app.config.console || IRBConsole.new(app) + @console = app.config.console || begin + require "rails/commands/console/irb_console" + IRBConsole.new(app) + end end def sandbox? diff --git a/railties/lib/rails/commands/console/irb_console.rb b/railties/lib/rails/commands/console/irb_console.rb new file mode 100644 index 0000000000..09c375ef25 --- /dev/null +++ b/railties/lib/rails/commands/console/irb_console.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require "irb/helper_method" +require "irb/command" + +module Rails + class Console + class RailsHelperBase < IRB::HelperMethod::Base + include ConsoleMethods + end + + class ControllerHelper < RailsHelperBase + description "Gets the helper methods available to the controller." + + # This method assumes an +ApplicationController+ exists, and that it extends ActionController::Base. + def execute + helper + end + end + + class ControllerInstance < RailsHelperBase + description "Gets a new instance of a controller object." + + # This method assumes an +ApplicationController+ exists, and that it extends ActionController::Base. + def execute + controller + end + end + + class NewSession < RailsHelperBase + description "Create a new session. If a block is given, the new session will be yielded to the block before being returned." + + def execute + new_session + end + end + + class AppInstance < RailsHelperBase + description "Reference the global 'app' instance, created on demand. To recreate the instance, pass a non-false value as the parameter." + + def execute(create = false) + app(create) + end + end + + class Reloader < IRB::Command::Base + include ConsoleMethods + + category "Rails console" + description "Reloads the environment." + + def execute(*) + reload! + end + end + + IRB::HelperMethod.register(:helper, ControllerHelper) + IRB::HelperMethod.register(:controller, ControllerInstance) + IRB::HelperMethod.register(:new_session, NewSession) + IRB::HelperMethod.register(:app, AppInstance) + IRB::Command.register(:reload!, Reloader) + + class IRBConsole + def initialize(app) + @app = app + + require "irb" + require "irb/completion" + end + + def name + "IRB" + end + + def start + IRB.setup(nil) + + if !Rails.env.local? && !ENV.key?("IRB_USE_AUTOCOMPLETE") + IRB.conf[:USE_AUTOCOMPLETE] = false + end + + env = colorized_env + app_name = @app.class.module_parent_name.underscore.dasherize + prompt_prefix = "#{app_name}(#{env})" + + IRB.conf[:PROMPT][:RAILS_PROMPT] = { + PROMPT_I: "#{prompt_prefix}> ", + PROMPT_S: "#{prompt_prefix}%l ", + PROMPT_C: "#{prompt_prefix}* ", + RETURN: "=> %s\n" + } + + if current_filter = IRB.conf[:BACKTRACE_FILTER] + IRB.conf[:BACKTRACE_FILTER] = -> (backtrace) do + backtrace = current_filter.call(backtrace) + Rails.backtrace_cleaner.filter(backtrace) + end + else + IRB.conf[:BACKTRACE_FILTER] = -> (backtrace) do + Rails.backtrace_cleaner.filter(backtrace) + end + end + + # Respect user's choice of prompt mode. + IRB.conf[:PROMPT_MODE] = :RAILS_PROMPT if IRB.conf[:PROMPT_MODE] == :DEFAULT + IRB::Irb.new.run(IRB.conf) + end + + def colorized_env + case Rails.env + when "development" + IRB::Color.colorize("dev", [:BLUE]) + when "test" + IRB::Color.colorize("test", [:BLUE]) + when "production" + IRB::Color.colorize("prod", [:RED]) + else + Rails.env + end + end + end + end +end diff --git a/railties/railties.gemspec b/railties/railties.gemspec index 044a89de1c..0ceed3cd2f 100644 --- a/railties/railties.gemspec +++ b/railties/railties.gemspec @@ -44,7 +44,7 @@ s.add_dependency "rake", ">= 12.2" s.add_dependency "thor", "~> 1.0", ">= 1.2.2" s.add_dependency "zeitwerk", "~> 2.6" - s.add_dependency "irb" + s.add_dependency "irb", "~> 1.13" s.add_development_dependency "actionview", version end diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index a079a390c2..b19942409b 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -5,99 +5,6 @@ require "isolation/abstract_unit" require "console_helpers" -class ConsoleTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Isolation - - def setup - build_app - end - - def teardown - teardown_app - end - - def load_environment(sandbox = false) - require "#{rails_root}/config/environment" - Rails.application.sandbox = sandbox - Rails.application.load_console - end - - def irb_context - Object.new.extend(Rails::ConsoleMethods) - end - - def test_app_method_should_return_integration_session - TestHelpers::Rack.remove_method :app - load_environment - console_session = irb_context.app - assert_instance_of ActionDispatch::Integration::Session, console_session - end - - def test_app_can_access_path_helper_method - app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get 'foo', to: 'foo#index' - end - RUBY - - load_environment - console_session = irb_context.app - assert_equal "/foo", console_session.foo_path - end - - def test_new_session_should_return_integration_session - load_environment - session = irb_context.new_session - assert_instance_of ActionDispatch::Integration::Session, session - end - - def test_reload_should_fire_preparation_and_cleanup_callbacks - load_environment - a = b = c = nil - - # TODO: These should be defined on the initializer - ActiveSupport::Reloader.to_complete { a = b = c = 1 } - ActiveSupport::Reloader.to_complete { b = c = 2 } - ActiveSupport::Reloader.to_prepare { c = 3 } - - irb_context.reload!(false) - - assert_equal 1, a - assert_equal 2, b - assert_equal 3, c - end - - def test_reload_should_reload_constants - app_file "app/models/user.rb", <<-MODEL - class User - attr_accessor :name - end - MODEL - - load_environment - assert_respond_to User.new, :name - - app_file "app/models/user.rb", <<-MODEL - class User - attr_accessor :name, :age - end - MODEL - - assert_not_respond_to User.new, :age - irb_context.reload!(false) - assert_respond_to User.new, :age - end - - def test_access_to_helpers - load_environment - helper = irb_context.helper - assert_not_nil helper - assert_instance_of ActionView::Base, helper - assert_equal "Once upon a time in a world...", - helper.truncate("Once upon a time in a world far far away") - end -end - class FullStackConsoleTest < ActiveSupport::TestCase include ConsoleHelpers @@ -233,6 +140,75 @@ def test_test_console_prompt write_prompt "123", "app-template(test)> 123" end + def test_helper_helper_method + spawn_console("-e development") + + write_prompt "helper.truncate('Once upon a time in a world far far away')", "Once upon a time in a world..." + end + + def test_controller_helper_method + spawn_console("-e development") + + write_prompt "controller.class.name", "ApplicationController" + end + + def test_new_session_helper_method + spawn_console("-e development") + + write_prompt "new_session.class.name", "ActionDispatch::Integration::Session" + end + + def test_app_helper_method + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get 'foo', to: 'foo#index' + end + RUBY + + spawn_console("-e development") + + write_prompt "app.foo_path", "/foo" + end + + def test_reload_command_fires_preparation_and_cleanup_callbacks + options = "-e development" + spawn_console(options) + + write_prompt "a = b = c = nil" + write_prompt "ActiveSupport::Reloader.to_complete { a = b = c = 1 }" + write_prompt "ActiveSupport::Reloader.to_complete { b = c = 2 }" + write_prompt "ActiveSupport::Reloader.to_prepare { c = 3 }" + write_prompt "reload!", "Reloading...\r\n" + write_prompt "a", "=> 1" + write_prompt "b", "=> 2" + write_prompt "c", "=> 3" + end + + def test_reload_command_reload_constants + app_file "app/models/user.rb", <<-MODEL + class User + attr_accessor :name + end + MODEL + + options = "-e development" + # Now the User model has only one attribute called `name` + spawn_console(options) + + + write_prompt "User.new.respond_to?(:age)", "=> false" + + # This will be loaded after the reload! command is executed + app_file "app/models/user.rb", <<-MODEL + class User + attr_accessor :name, :age + end + MODEL + + write_prompt "reload!", "Reloading...\r\n" + write_prompt "User.new.respond_to?(:age)", "=> true" + end + def test_console_respects_user_defined_prompt_mode irbrc = Tempfile.new("irbrc") irbrc.write <<-RUBY