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"