diff --git a/Gemfile b/Gemfile index c0bea54..c9fa00f 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem "appraisal" gem "actionview", "~> 8.1" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", "~> 8.1" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/Gemfile.lock b/Gemfile.lock index cc04bb4..18cac3d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -93,6 +93,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -203,6 +204,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties (~> 8.1) rake (~> 13.0) reactionview! diff --git a/README.md b/README.md index 778f191..2c80b10 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,9 @@ ReActionView.configure do |config| # Enable debug mode config.debug_mode = Rails.env.development? + # Custom path for editor "open in editor" links (optional, defaults to Rails.root) + # config.project_path = ENV.fetch('PROJECT_PATH', Rails.root.to_s) + # Validation mode (:raise, :overlay, or :none) — defaults to :raise in test, :overlay otherwise # config.validation_mode = :overlay end diff --git a/docs/docs/installation.md b/docs/docs/installation.md index 50c58fd..e16182a 100644 --- a/docs/docs/installation.md +++ b/docs/docs/installation.md @@ -67,6 +67,32 @@ end This gives you all the benefits of Herb's validation, security features, and debugging tools for your existing templates. +### Advanced Configuration + +#### Custom Project Path for Editor Links + +When your app runs somewhere other than where its files live, such as a Docker bind mount, a devcontainer, or a VM, the paths Rails sees aren't paths your editor can open. `config.project_path` says where `Rails.root` is mounted on the machine running your editor, and rewrites "open in editor" links to match: + +:::code-group +```ruby [config/initializers/reactionview.rb] +ReActionView.configure do |config| + # Where Rails.root is mounted on the machine running your editor + config.project_path = "/Users/you/myapp" + + # Or take it from the environment + # config.project_path = ENV.fetch("PROJECT_PATH", Rails.root.to_s) +end +``` +::: + +With `Rails.root` at `/app` inside the container, a template at `/app/app/views/users/show.html.erb` then opens as `/Users/you/myapp/app/views/users/show.html.erb`. + +**Default**: `Rails.root.to_s` + +::: info Only editor links are affected +Local template detection and the `herb-project-path` meta tag stay on `Rails.root`. The meta tag is compared against the path the `herb dev` server reports, so overriding it would make the dev tools treat the page as a different project and ignore it. +::: + ## Verify Installation Create a test template to verify ReActionView is working: diff --git a/gemfiles/rails_7_0.gemfile b/gemfiles/rails_7_0.gemfile index 16b7619..3d69615 100644 --- a/gemfiles/rails_7_0.gemfile +++ b/gemfiles/rails_7_0.gemfile @@ -6,6 +6,7 @@ gem "actionview", "~> 7.0.0" gem "appraisal" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", "~> 7.0.0" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/gemfiles/rails_7_0.gemfile.lock b/gemfiles/rails_7_0.gemfile.lock index 22eeca0..f2dcfdf 100644 --- a/gemfiles/rails_7_0.gemfile.lock +++ b/gemfiles/rails_7_0.gemfile.lock @@ -85,6 +85,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) mutex_m (0.3.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) @@ -175,6 +176,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties (~> 7.0.0) rake (~> 13.0) reactionview! diff --git a/gemfiles/rails_7_1.gemfile b/gemfiles/rails_7_1.gemfile index 98f051b..6d0b61f 100644 --- a/gemfiles/rails_7_1.gemfile +++ b/gemfiles/rails_7_1.gemfile @@ -6,6 +6,7 @@ gem "actionview", "~> 7.1.0" gem "appraisal" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", "~> 7.1.0" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/gemfiles/rails_7_1.gemfile.lock b/gemfiles/rails_7_1.gemfile.lock index 21da859..5992830 100644 --- a/gemfiles/rails_7_1.gemfile.lock +++ b/gemfiles/rails_7_1.gemfile.lock @@ -97,6 +97,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) mutex_m (0.3.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) @@ -207,6 +208,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties (~> 7.1.0) rake (~> 13.0) reactionview! diff --git a/gemfiles/rails_7_2.gemfile b/gemfiles/rails_7_2.gemfile index c54166e..a9b174f 100644 --- a/gemfiles/rails_7_2.gemfile +++ b/gemfiles/rails_7_2.gemfile @@ -6,6 +6,7 @@ gem "actionview", "~> 7.2.0" gem "appraisal" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", "~> 7.2.0" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/gemfiles/rails_7_2.gemfile.lock b/gemfiles/rails_7_2.gemfile.lock index 4d94390..209cb29 100644 --- a/gemfiles/rails_7_2.gemfile.lock +++ b/gemfiles/rails_7_2.gemfile.lock @@ -97,6 +97,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -207,6 +208,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties (~> 7.2.0) rake (~> 13.0) reactionview! diff --git a/gemfiles/rails_8_0.gemfile b/gemfiles/rails_8_0.gemfile index a2a9980..21b154a 100644 --- a/gemfiles/rails_8_0.gemfile +++ b/gemfiles/rails_8_0.gemfile @@ -6,6 +6,7 @@ gem "actionview", "~> 8.0.0" gem "appraisal" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", "~> 8.0.0" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/gemfiles/rails_8_0.gemfile.lock b/gemfiles/rails_8_0.gemfile.lock index ad37d48..b3cb8e8 100644 --- a/gemfiles/rails_8_0.gemfile.lock +++ b/gemfiles/rails_8_0.gemfile.lock @@ -94,6 +94,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -204,6 +205,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties (~> 8.0.0) rake (~> 13.0) reactionview! diff --git a/gemfiles/rails_8_1.gemfile b/gemfiles/rails_8_1.gemfile index 0207b8e..977063f 100644 --- a/gemfiles/rails_8_1.gemfile +++ b/gemfiles/rails_8_1.gemfile @@ -6,6 +6,7 @@ gem "actionview", "~> 8.1.0" gem "appraisal" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", "~> 8.1.0" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/gemfiles/rails_8_1.gemfile.lock b/gemfiles/rails_8_1.gemfile.lock index 81a473a..4043cdf 100644 --- a/gemfiles/rails_8_1.gemfile.lock +++ b/gemfiles/rails_8_1.gemfile.lock @@ -93,6 +93,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -203,6 +204,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties (~> 8.1.0) rake (~> 13.0) reactionview! diff --git a/gemfiles/rails_8_2.gemfile b/gemfiles/rails_8_2.gemfile index 913bcaf..32130c6 100644 --- a/gemfiles/rails_8_2.gemfile +++ b/gemfiles/rails_8_2.gemfile @@ -6,6 +6,7 @@ gem "actionview", github: "rails/rails" gem "appraisal" gem "maxitest", "~> 7.0" gem "minitest-difftastic" +gem "minitest-mock" gem "railties", github: "rails/rails" gem "rake", "~> 13.0" gem "readline", "~> 0.0.4" diff --git a/gemfiles/rails_8_2.gemfile.lock b/gemfiles/rails_8_2.gemfile.lock index 7d28a5b..b230149 100644 --- a/gemfiles/rails_8_2.gemfile.lock +++ b/gemfiles/rails_8_2.gemfile.lock @@ -110,6 +110,7 @@ GEM prism (~> 1.5) minitest-difftastic (0.2.1) difftastic (~> 0.6) + minitest-mock (5.27.0) nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) nokogiri (1.19.4-aarch64-linux-musl) @@ -216,6 +217,7 @@ DEPENDENCIES appraisal maxitest (~> 7.0) minitest-difftastic + minitest-mock railties! rake (~> 13.0) reactionview! diff --git a/javascript/packages/dev-tools/src/index.ts b/javascript/packages/dev-tools/src/index.ts index 7adf7b7..f415d73 100644 --- a/javascript/packages/dev-tools/src/index.ts +++ b/javascript/packages/dev-tools/src/index.ts @@ -86,11 +86,7 @@ if (typeof window !== "undefined" && typeof document !== "undefined") { isInitializing = true try { - let projectPath: string | undefined - const railsRoot = document.querySelector(`meta[name="herb-rails-root"]`)?.getAttribute("content") - if (railsRoot) { - projectPath = railsRoot - } + const projectPath = document.querySelector(`meta[name="herb-project-path"]`)?.getAttribute("content") ?? undefined initReActionViewDevTools({ projectPath, diff --git a/lib/generators/reactionview/install_generator.rb b/lib/generators/reactionview/install_generator.rb index ccc5b1b..4ca6060 100644 --- a/lib/generators/reactionview/install_generator.rb +++ b/lib/generators/reactionview/install_generator.rb @@ -23,6 +23,9 @@ def create_initializer # Enable debug mode in development (adds debug attributes to HTML) config.debug_mode = Rails.env.development? + # Path used for editor "open in editor" links (optional, defaults to Rails.root) + # config.project_path = ENV.fetch('PROJECT_PATH', Rails.root.to_s) + # Validation mode (:raise, :overlay, or :none) — defaults to :raise in test, :overlay otherwise # config.validation_mode = :overlay diff --git a/lib/reactionview/config.rb b/lib/reactionview/config.rb index 0c8e9b5..d903af4 100644 --- a/lib/reactionview/config.rb +++ b/lib/reactionview/config.rb @@ -5,12 +5,18 @@ class Config attr_accessor :intercept_erb attr_accessor :debug_mode attr_accessor :transform_visitors + attr_writer :project_path attr_writer :validation_mode def initialize @intercept_erb = false @debug_mode = nil @transform_visitors = [] + @project_path = nil + end + + def project_path + @project_path || Rails.root.to_s end def validation_mode diff --git a/lib/reactionview/template/handlers/herb.rb b/lib/reactionview/template/handlers/herb.rb index b34f4c2..bb6ac61 100644 --- a/lib/reactionview/template/handlers/herb.rb +++ b/lib/reactionview/template/handlers/herb.rb @@ -13,8 +13,8 @@ def call(template, source) if ::ReActionView.config.debug_mode_enabled? && local_template?(template) visitors << ::Herb::Engine::DebugVisitor.new( - file_path: template.identifier, - project_path: Rails.root.to_s + file_path: translate_path_for_editor(template.identifier), + project_path: ::ReActionView.config.project_path ) end @@ -58,6 +58,15 @@ def editor_meta_tag %() end + def translate_path_for_editor(template_path) + rails_root = Rails.root.to_s + project_path = ::ReActionView.config.project_path + + return template_path if project_path == rails_root + + template_path.to_s.sub(rails_root, project_path) + end + def reactionview_dev_tools_markup(template) return nil unless layout_template?(template) return nil unless local_template?(template) diff --git a/test/reactionview/config_test.rb b/test/reactionview/config_test.rb index 613e003..7164c53 100644 --- a/test/reactionview/config_test.rb +++ b/test/reactionview/config_test.rb @@ -58,4 +58,31 @@ def config.test? assert_equal :raise, config.validation_mode end + + test "project_path defaults to Rails.root" do + config = ReActionView::Config.new + + Rails.stub(:root, Pathname.new("/app")) do + assert_equal "/app", config.project_path + end + end + + test "project_path returns the configured value" do + config = ReActionView::Config.new + config.project_path = "/Users/you/myapp" + + Rails.stub(:root, Pathname.new("/app")) do + assert_equal "/Users/you/myapp", config.project_path + end + end + + test "project_path falls back to Rails.root when reset to nil" do + config = ReActionView::Config.new + config.project_path = "/Users/you/myapp" + config.project_path = nil + + Rails.stub(:root, Pathname.new("/app")) do + assert_equal "/app", config.project_path + end + end end diff --git a/test/snapshot_utils.rb b/test/snapshot_utils.rb index 578d430..8ccc6fd 100644 --- a/test/snapshot_utils.rb +++ b/test/snapshot_utils.rb @@ -15,10 +15,10 @@ def ask?(prompt = "") end module SnapshotUtils # rubocop:disable Metrics/ModuleLength - def assert_compiled_snapshot(source, handler: ReActionView::Template::Handlers::ERB, virtual_path: "test", format: :html, locals: [], options: {}) # rubocop:disable Metrics/ParameterLists + def assert_compiled_snapshot(source, handler: ReActionView::Template::Handlers::ERB, virtual_path: "test", identifier: "test_template", format: :html, locals: [], options: {}) # rubocop:disable Metrics/ParameterLists,Layout/LineLength template = ActionView::Template.new( source, - "test_template", + identifier, handler, virtual_path: virtual_path, format: format, @@ -53,10 +53,10 @@ def assert_compiled_snapshot(source, handler: ReActionView::Template::Handlers:: compiled_source end - def assert_evaluated_snapshot(source, ivars: {}, options: {}, handler: ReActionView::Template::Handlers::ERB, virtual_path: "test", format: :html, locals: []) # rubocop:disable Metrics/ParameterLists,Layout/LineLength,Metrics/MethodLength + def assert_evaluated_snapshot(source, ivars: {}, options: {}, handler: ReActionView::Template::Handlers::ERB, virtual_path: "test", identifier: "test_template", format: :html, locals: []) # rubocop:disable Metrics/ParameterLists,Layout/LineLength,Metrics/MethodLength template = ActionView::Template.new( source, - "test_template", + identifier, handler, virtual_path: virtual_path, format: format, diff --git a/test/snapshots/re_action_view/project_path_test/test_0006_compiled_output_without_a_configured_project_path_compiled_23f372c5835bd8e5809427a8bfe433ca.txt b/test/snapshots/re_action_view/project_path_test/test_0006_compiled_output_without_a_configured_project_path_compiled_23f372c5835bd8e5809427a8bfe433ca.txt new file mode 100644 index 0000000..777a6b6 --- /dev/null +++ b/test/snapshots/re_action_view/project_path_test/test_0006_compiled_output_without_a_configured_project_path_compiled_23f372c5835bd8e5809427a8bfe433ca.txt @@ -0,0 +1,2 @@ + @output_buffer.safe_append='

Hello

'.freeze; +@output_buffer \ No newline at end of file diff --git a/test/snapshots/re_action_view/project_path_test/test_0007_compiled_output_with_a_configured_project_path_compiled_15299f7b9059f08ba4360b398f86c031.txt b/test/snapshots/re_action_view/project_path_test/test_0007_compiled_output_with_a_configured_project_path_compiled_15299f7b9059f08ba4360b398f86c031.txt new file mode 100644 index 0000000..8f926eb --- /dev/null +++ b/test/snapshots/re_action_view/project_path_test/test_0007_compiled_output_with_a_configured_project_path_compiled_15299f7b9059f08ba4360b398f86c031.txt @@ -0,0 +1,2 @@ + @output_buffer.safe_append='

Hello

'.freeze; +@output_buffer \ No newline at end of file diff --git a/test/snapshots/re_action_view/project_path_test/test_0008_evaluated_output_with_a_configured_project_path_evaluated_4d3a28975f5f90eefab8a7f79760fa4e.txt b/test/snapshots/re_action_view/project_path_test/test_0008_evaluated_output_with_a_configured_project_path_evaluated_4d3a28975f5f90eefab8a7f79760fa4e.txt new file mode 100644 index 0000000..19e465b --- /dev/null +++ b/test/snapshots/re_action_view/project_path_test/test_0008_evaluated_output_with_a_configured_project_path_evaluated_4d3a28975f5f90eefab8a7f79760fa4e.txt @@ -0,0 +1 @@ +

Hello

\ No newline at end of file diff --git a/test/template/handlers/project_path_test.rb b/test/template/handlers/project_path_test.rb new file mode 100644 index 0000000..241ca65 --- /dev/null +++ b/test/template/handlers/project_path_test.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +require "action_controller" + +class ReActionView::ProjectPathTest < Minitest::Spec + RAILS_ROOT = "/app" + HOST_PATH = "/Users/you/myapp" + VIEW = "/app/app/views/users/show.html.erb" + LAYOUT = "/app/app/views/layouts/application.html.erb" + + before do + @previous_debug_mode = ReActionView.config.debug_mode + + ReActionView.config.debug_mode = true + ReActionView.config.project_path = nil + end + + after do + ReActionView.config.debug_mode = @previous_debug_mode + ReActionView.config.project_path = nil + end + + def compile(source, identifier: VIEW, virtual_path: "users/show") + template = ActionView::Template.new( + source, + identifier, + ReActionView::Template::Handlers::Herb, + virtual_path: virtual_path, + format: :html, + locals: [] + ) + + Rails.stub(:root, Pathname.new(RAILS_ROOT)) do + ReActionView::Template::Handlers::Herb.call(template, source) + end + end + + test "editor path is left alone when project_path is not configured" do + compiled = compile("
Hello
") + + assert_includes compiled, %(data-herb-debug-file-full-path="/app/app/views/users/show.html.erb") + end + + test "editor path is rewritten to the configured project_path" do + ReActionView.config.project_path = HOST_PATH + + compiled = compile("
Hello
") + + assert_includes compiled, %(data-herb-debug-file-full-path="/Users/you/myapp/app/views/users/show.html.erb") + end + + test "relative path stays correct when the editor path is rewritten" do + ReActionView.config.project_path = HOST_PATH + + compiled = compile("
Hello
") + + assert_includes compiled, %(data-herb-debug-file-relative-path="app/views/users/show.html.erb") + end + + test "herb-project-path meta tag keeps Rails.root so it matches the herb dev server" do + ReActionView.config.project_path = HOST_PATH + + compiled = compile("", identifier: LAYOUT, virtual_path: "layouts/application") + + assert_includes compiled, %() + refute_includes compiled, %() + end + + test "templates outside Rails.root stay undecorated even when project_path matches them" do + ReActionView.config.project_path = HOST_PATH + + compiled = compile("
Hello
", identifier: "#{HOST_PATH}/app/views/users/show.html.erb") + + refute_includes compiled, "data-herb-debug" + end + + test "compiled output without a configured project_path" do + Rails.stub(:root, Pathname.new(RAILS_ROOT)) do + assert_compiled_snapshot( + "

Hello

", + handler: ReActionView::Template::Handlers::Herb, + identifier: VIEW, + virtual_path: "users/show", + options: { project_path: nil } + ) + end + end + + test "compiled output with a configured project_path" do + ReActionView.config.project_path = HOST_PATH + + Rails.stub(:root, Pathname.new(RAILS_ROOT)) do + assert_compiled_snapshot( + "

Hello

", + handler: ReActionView::Template::Handlers::Herb, + identifier: VIEW, + virtual_path: "users/show", + options: { project_path: HOST_PATH } + ) + end + end + + test "evaluated output with a configured project_path" do + ReActionView.config.project_path = HOST_PATH + + Rails.stub(:root, Pathname.new(RAILS_ROOT)) do + assert_evaluated_snapshot( + "

Hello

", + handler: ReActionView::Template::Handlers::Herb, + identifier: VIEW, + virtual_path: "users/show", + options: { project_path: HOST_PATH } + ) + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index c8dfc5f..2199d3e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -10,6 +10,7 @@ require "maxitest/autorun" require "minitest/spec" +require "minitest/mock" require_relative "snapshot_utils"