mirror of https://github.com/rails/rails
Register Rails console commands/helpers with its latest extension APIs
This will greatly increase the visibility of Rails console commands and helpers, and stop rely on IRB's internal components. Extension API reference: https://github.com/ruby/irb/blob/master/EXTEND_IRB.md And because we need to create new classes to use the new APIs, I also moved all the IRB-specific code to a new file, `irb_console.rb`. Use IRB.conf[:BACKTRACE_FILTER] for backtrace filtering in console This change uses the new `IRB.conf[:BACKTRACE_FILTER]` to inject the backtrace filtering logic into IRB. This avoids the need to patch IRB's internal WorkSpace class. Update changelog
This commit is contained in:
parent
f84e1ebb5a
commit
4c1f7d8328
14
Gemfile.lock
14
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)
|
||||
|
|
|
@ -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*
|
||||
|
|
|
@ -4,66 +4,6 @@ require "rails/command/environment_argument"
|
|||
|
||||
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 @@ module Rails
|
|||
|
||||
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?
|
||||
|
|
|
@ -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
|
|
@ -44,7 +44,7 @@ Gem::Specification.new do |s|
|
|||
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
|
||||
|
|
|
@ -5,99 +5,6 @@ require "tempfile"
|
|||
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 @@ class FullStackConsoleTest < ActiveSupport::TestCase
|
|||
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
|
||||
|
|
Loading…
Reference in New Issue