From 486fa2b766fc8e4a19a5ba898cc80a63a3837abd Mon Sep 17 00:00:00 2001 From: Justin Gordon Date: Thu, 18 Jun 2026 23:50:45 -1000 Subject: [PATCH 01/24] Fix generated demo path hints --- .../react_on_rails/base_generator.rb | 21 ++--- .../react_on_rails/demo_page_config.rb | 8 +- .../react_on_rails/generator_helper.rb | 81 +++++++++++++++++++ .../react_no_redux_generator.rb | 18 +++-- .../react_with_redux_generator.rb | 22 ++--- .../generators/react_on_rails/rsc_setup.rb | 7 +- .../app/views/hello_world/index.html.erb.tt | 7 +- .../base/app/views/home/index.html.erb.tt | 4 + .../generators/install_generator_spec.rb | 58 +++++++++++++ .../react_no_redux_generator_examples.rb | 2 + .../react_with_redux_generator_examples.rb | 2 + 11 files changed, 196 insertions(+), 34 deletions(-) diff --git a/react_on_rails/lib/generators/react_on_rails/base_generator.rb b/react_on_rails/lib/generators/react_on_rails/base_generator.rb index 417cbc60a2..11dc13dac7 100644 --- a/react_on_rails/lib/generators/react_on_rails/base_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/base_generator.rb @@ -192,7 +192,7 @@ def create_react_directories # Skip HelloWorld directory for Redux (uses HelloWorldApp) or RSC (uses HelloServer) return if options.redux? || use_rsc? - empty_directory("app/javascript/src/HelloWorld/ror_components") + empty_directory(File.join(example_component_source_directory("HelloWorld"), "ror_components")) end def copy_base_files @@ -235,14 +235,15 @@ def copy_base_files def copy_js_bundle_files base_path = "base/base/" - base_files = %w[app/javascript/packs/server-bundle.js] + copy_file("#{base_path}app/javascript/packs/server-bundle.js", + shakapacker_entrypoint_path("server-bundle.js")) # Skip HelloWorld CSS for Redux (uses HelloWorldApp) or RSC (uses HelloServer) - unless options.redux? || use_rsc? || use_tailwind? - base_files << "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css" - end + return if options.redux? || use_rsc? || use_tailwind? - base_files.each { |file| copy_file("#{base_path}#{file}", file) } + copy_file("#{base_path}app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css", + File.join(example_component_source_directory("HelloWorld"), + "ror_components/HelloWorld.module.css")) end def copy_webpack_config @@ -274,7 +275,7 @@ def copy_tailwind_files base_path = "base/tailwind/" copy_file("#{base_path}app/javascript/stylesheets/application.css", - "app/javascript/stylesheets/application.css") + shakapacker_stylesheet_path("application.css")) end def copy_packer_config @@ -686,10 +687,10 @@ def home_page_stack_badges end def example_source_path - return "app/javascript/src/HelloServer/" if use_rsc? && !options.redux? - return "app/javascript/src/HelloWorldApp/" if options.redux? + return example_component_source_path("HelloServer") if use_rsc? && !options.redux? + return example_component_source_path("HelloWorldApp") if options.redux? - "app/javascript/src/HelloWorld/" + example_component_source_path("HelloWorld") end def example_view_path diff --git a/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb b/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb index 991ec58768..bf6e5c8fa4 100644 --- a/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb +++ b/react_on_rails/lib/generators/react_on_rails/demo_page_config.rb @@ -15,13 +15,13 @@ def build_hello_world_view_config(component_name:, source_path:, landing_page:, } end - def build_hello_server_view_config(landing_page:, redux_demo:) + def build_hello_server_view_config(landing_page:, redux_demo:, source_path:) { title: "React Server Components Demo", intro: "This route shows the Pro React Server Components flow: Rails streams an async server " \ "component response while only client islands ship JavaScript to the browser.", highlights: hello_server_highlights, - file_hints: hello_server_file_hints, + file_hints: hello_server_file_hints(source_path:), quick_links: hello_server_quick_links(landing_page:, redux_demo:), learning_links: hello_server_learning_links } @@ -167,10 +167,10 @@ def hello_server_highlights ] end - def hello_server_file_hints + def hello_server_file_hints(source_path:) [ { - path: "app/javascript/src/HelloServer/", + path: source_path, description: "Source for the generated server component example and client island." }, { diff --git a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb index c7e93f8666..ea06694f5c 100644 --- a/react_on_rails/lib/generators/react_on_rails/generator_helper.rb +++ b/react_on_rails/lib/generators/react_on_rails/generator_helper.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true require "json" +require "pathname" require_relative "shakapacker_precompile_hook_helper" # rubocop:disable Metrics/ModuleLength module GeneratorHelper include ReactOnRails::Generators::ShakapackerPrecompileHookHelper + DEFAULT_SHAKAPACKER_SOURCE_PATH = "app/javascript" + DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH = "packs" + def package_json # Lazy load package_json gem only when actually needed for dependency management @@ -75,6 +79,83 @@ def component_extension(options) options.typescript? ? "tsx" : "jsx" end + def shakapacker_source_path + configured_shakapacker_relative_path("source_path", DEFAULT_SHAKAPACKER_SOURCE_PATH) + end + + def shakapacker_source_entry_path + configured_shakapacker_relative_path("source_entry_path", DEFAULT_SHAKAPACKER_SOURCE_ENTRY_PATH) + end + + def shakapacker_entrypoint_path(filename) + File.join(shakapacker_source_path, shakapacker_source_entry_path, filename) + end + + def shakapacker_stylesheet_path(filename) + File.join(shakapacker_source_path, "stylesheets", filename) + end + + def example_component_source_directory(component_name) + File.join(shakapacker_source_path, "src", component_name) + end + + def example_component_source_path(component_name) + "#{example_component_source_directory(component_name)}/" + end + + def configured_shakapacker_relative_path(config_key, default) + config_path = File.join(destination_root, "config/shakapacker.yml") + return default unless File.exist?(config_path) + + config = parse_shakapacker_yml(config_path) + configured_path = shakapacker_path_config_value(config, config_key) + + safe_generator_destination_path(configured_path, default:) + end + + def shakapacker_path_config_value(config, config_key) + %w[development default].each do |section_name| + section = shakapacker_config_section(config, section_name) + value = shakapacker_config_value(section, config_key) + return value unless value.to_s.strip.empty? + end + + nil + end + + def safe_generator_destination_path(path, default:) + candidate = path.to_s.strip + return default if candidate.empty? + + pathname = Pathname.new(candidate).cleanpath + relative_path = if pathname.absolute? + absolute_path_relative_to_destination(pathname) + else + pathname.to_s + end + + return default if unsafe_generator_destination_path?(relative_path) + + relative_path + rescue ArgumentError + default + end + + def absolute_path_relative_to_destination(pathname) + destination = Pathname.new(destination_root).cleanpath + relative_path = pathname.relative_path_from(destination).to_s + + return nil if unsafe_generator_destination_path?(relative_path) + + relative_path + rescue ArgumentError + nil + end + + def unsafe_generator_destination_path?(path) + path.nil? || path == "." || path == ".." || path.start_with?("../") + end + # Check if a gem is present in Gemfile.lock # Always checks the target app's Gemfile.lock, not inherited BUNDLE_GEMFILE # See: https://github.com/shakacode/react_on_rails/issues/2287 diff --git a/react_on_rails/lib/generators/react_on_rails/react_no_redux_generator.rb b/react_on_rails/lib/generators/react_on_rails/react_no_redux_generator.rb index e0d05a0c66..477fb86a66 100644 --- a/react_on_rails/lib/generators/react_on_rails/react_no_redux_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/react_no_redux_generator.rb @@ -32,22 +32,26 @@ def copy_base_files base_js_path = "base/base" tailwind_js_path = "base/tailwind" ext = component_extension(options) + component_dir = example_component_source_directory("HelloWorld") # Determine which component files to copy based on TypeScript option client_component = - "app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{ext}" + "#{component_dir}/ror_components/HelloWorld.client.#{ext}" server_component = - "app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{ext}" + "#{component_dir}/ror_components/HelloWorld.server.#{ext}" if use_tailwind? - copy_file("#{tailwind_js_path}/#{client_component}", client_component) + copy_file("#{tailwind_js_path}/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{ext}", + client_component) else - copy_file("#{base_js_path}/#{client_component}", client_component) + copy_file("#{base_js_path}/app/javascript/src/HelloWorld/ror_components/HelloWorld.client.#{ext}", + client_component) copy_file("#{base_js_path}/app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css", - "app/javascript/src/HelloWorld/ror_components/HelloWorld.module.css") + "#{component_dir}/ror_components/HelloWorld.module.css") end - copy_file("#{base_js_path}/#{server_component}", server_component) + copy_file("#{base_js_path}/app/javascript/src/HelloWorld/ror_components/HelloWorld.server.#{ext}", + server_component) end def create_appropriate_templates @@ -58,7 +62,7 @@ def create_appropriate_templates "app/views/hello_world/index.html.erb", build_hello_world_view_config( component_name: "HelloWorld", - source_path: "app/javascript/src/HelloWorld/", + source_path: example_component_source_path("HelloWorld"), landing_page: new_app_landing_page_available?, redux: false, rsc_demo: false diff --git a/react_on_rails/lib/generators/react_on_rails/react_with_redux_generator.rb b/react_on_rails/lib/generators/react_on_rails/react_with_redux_generator.rb index ad9e9fec47..e2a7f40c02 100644 --- a/react_on_rails/lib/generators/react_on_rails/react_with_redux_generator.rb +++ b/react_on_rails/lib/generators/react_on_rails/react_with_redux_generator.rb @@ -41,31 +41,34 @@ class ReactWithReduxGenerator < Rails::Generators::Base hide: true def create_redux_directories + component_dir = example_component_source_directory("HelloWorldApp") + # Create auto-bundling directory structure for Redux - empty_directory("app/javascript/src/HelloWorldApp/ror_components") + empty_directory("#{component_dir}/ror_components") # Create Redux support directories within the component directory dirs = %w[actions constants containers reducers store components] - dirs.each { |name| empty_directory("app/javascript/src/HelloWorldApp/#{name}") } + dirs.each { |name| empty_directory("#{component_dir}/#{name}") } end def copy_base_files base_js_path = "redux/base" ext = component_extension(options) + component_dir = example_component_source_directory("HelloWorldApp") # Copy Redux-connected component to auto-bundling structure copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.client.#{ext}", - "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}") + "#{component_dir}/ror_components/HelloWorldApp.client.#{ext}") copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/startup/HelloWorldApp.server.#{ext}", - "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.server.#{ext}") + "#{component_dir}/ror_components/HelloWorldApp.server.#{ext}") unless use_tailwind? copy_file("#{base_js_path}/app/javascript/bundles/HelloWorld/components/HelloWorld.module.css", - "app/javascript/src/HelloWorldApp/components/HelloWorld.module.css") + "#{component_dir}/components/HelloWorld.module.css") end # Update import paths in client component - ror_client_file = "app/javascript/src/HelloWorldApp/ror_components/HelloWorldApp.client.#{ext}" + ror_client_file = "#{component_dir}/ror_components/HelloWorldApp.client.#{ext}" gsub_file(ror_client_file, "../store/helloWorldStore", "../store/helloWorldStore") gsub_file(ror_client_file, "../containers/HelloWorldContainer", "../containers/HelloWorldContainer") @@ -86,6 +89,7 @@ def copy_base_redux_files base_hello_world_path = "redux/base/app/javascript/bundles/HelloWorld" tailwind_hello_world_path = "redux/tailwind/app/javascript/bundles/HelloWorld" redux_extension = options.typescript? ? "ts" : "js" + component_dir = example_component_source_directory("HelloWorldApp") # Copy Redux infrastructure files with appropriate extension %W[actions/helloWorldActionCreators.#{redux_extension} @@ -94,13 +98,13 @@ def copy_base_redux_files reducers/helloWorldReducer.#{redux_extension} store/helloWorldStore.#{redux_extension}].each do |file| copy_file("#{base_hello_world_path}/#{file}", - "app/javascript/src/HelloWorldApp/#{file}") + "#{component_dir}/#{file}") end component_file = "components/HelloWorld.#{component_extension(options)}" component_source_path = use_tailwind? ? tailwind_hello_world_path : base_hello_world_path copy_file("#{component_source_path}/#{component_file}", - "app/javascript/src/HelloWorldApp/#{component_file}") + "#{component_dir}/#{component_file}") end def create_appropriate_templates @@ -111,7 +115,7 @@ def create_appropriate_templates "app/views/hello_world/index.html.erb", build_hello_world_view_config( component_name: "HelloWorldApp", - source_path: "app/javascript/src/HelloWorldApp/", + source_path: example_component_source_path("HelloWorldApp"), landing_page: new_app_landing_page_available?, redux: true, rsc_demo: options[:rsc] diff --git a/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb b/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb index d8d8825e53..72efcf81bb 100644 --- a/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb +++ b/react_on_rails/lib/generators/react_on_rails/rsc_setup.rb @@ -198,7 +198,7 @@ def add_rsc_to_procfile end def create_hello_server_component - hello_server_dir = "app/javascript/src/HelloServer" + hello_server_dir = example_component_source_directory("HelloServer") ror_components_dir = "#{hello_server_dir}/ror_components" components_dir = "#{hello_server_dir}/components" ext = component_extension(options) @@ -246,7 +246,7 @@ def add_tailwind_import_to_rsc_client_component(components_dir) end return false unless relative_entry_path - # Path is relative to app/javascript/src/HelloServer/components/. + # Path is relative to the generated HelloServer components directory. stylesheet_import = "import '../../../stylesheets/application.css';" entry_path = File.join(destination_root, relative_entry_path) entry_content = File.read(entry_path) @@ -296,7 +296,8 @@ def create_hello_server_view view_path, build_hello_server_view_config( landing_page: new_app_landing_page_available?, - redux_demo: options[:redux] + redux_demo: options[:redux], + source_path: example_component_source_path("HelloServer") )) say "✅ Created #{view_path}", :green diff --git a/react_on_rails/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt b/react_on_rails/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt index 40542a99db..ea093b357b 100644 --- a/react_on_rails/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt +++ b/react_on_rails/lib/generators/react_on_rails/templates/base/base/app/views/hello_world/index.html.erb.tt @@ -129,6 +129,11 @@ background: rgba(17, 32, 49, 0.06); color: var(--ink); font-size: 0.95em; + } + + .path-hint { + display: inline-block; + max-width: 100%; overflow-wrap: anywhere; word-break: break-word; } @@ -181,7 +186,7 @@