diff --git a/CHANGELOG.md b/CHANGELOG.md index b324a8e433..9c6d318fd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Changes since the last non-beta release. - **Improved Error Messages**: Error messages for version mismatches and package configuration issues now include package-manager-specific installation commands (npm, yarn, pnpm, bun). [PR #1881](https://github.com/shakacode/react_on_rails/pull/1881) by [AbanoubGhadban](https://github.com/AbanoubGhadban). +- **Smart Error Messages with Actionable Solutions**: Added intelligent Ruby-side error handling with context-aware, actionable solutions for common issues. Features include fuzzy matching for component name typos, environment-specific debugging suggestions, color-coded error formatting, and detailed troubleshooting guides for component registration, auto-bundling, hydration mismatches, server rendering errors, and Redux store issues. See the [Improved Error Messages guide](docs/guides/improved-error-messages.md) for details. [PR 1934](https://github.com/shakacode/react_on_rails/pull/1934) by [justin808](https://github.com/justin808). + - **Improved RSC Payload Error Handling**: Errors that happen during generation of RSC payload are transferred properly to rails side and logs the error message and stack. [PR #1888](https://github.com/shakacode/react_on_rails/pull/1888) by [AbanoubGhadban](https://github.com/AbanoubGhadban). #### Changed diff --git a/bin/lefthook/eslint-lint b/bin/lefthook/eslint-lint index 4147c46e38..674fddfadf 100755 --- a/bin/lefthook/eslint-lint +++ b/bin/lefthook/eslint-lint @@ -10,50 +10,56 @@ if [ -z "$files" ]; then exit 0 fi -# Separate files into root and Pro directories -root_files=$(echo "$files" | grep -v '^react_on_rails_pro/' || true) -pro_files=$(echo "$files" | grep '^react_on_rails_pro/' || true) +# Separate files into different directories +# react_on_rails_pro/ has its own ESLint config +# packages/react-on-rails-pro/ uses root ESLint config +react_on_rails_pro_files=$(echo "$files" | grep '^react_on_rails_pro/' || true) +packages_pro_files=$(echo "$files" | grep '^packages/react-on-rails-pro/' || true) +root_files=$(echo "$files" | grep -v '^react_on_rails_pro/' | grep -v '^packages/react-on-rails-pro/' || true) exit_code=0 -# Lint root files -if [ -n "$root_files" ]; then +# Lint root files (includes packages/react-on-rails-pro) +root_and_packages_pro_files="$root_files $packages_pro_files" +root_and_packages_pro_files=$(echo "$root_and_packages_pro_files" | xargs) # trim whitespace + +if [ -n "$root_and_packages_pro_files" ]; then if [ "$CONTEXT" = "all-changed" ]; then echo "🔍 ESLint on root changed files:" else echo "🔍 ESLint on root $CONTEXT files:" fi - printf " %s\n" $root_files + printf " %s\n" $root_and_packages_pro_files - if ! yarn run eslint $root_files --report-unused-disable-directives --fix; then + if ! yarn run eslint $root_and_packages_pro_files --report-unused-disable-directives --fix; then exit_code=1 fi # Re-stage files if running on staged or all-changed context if [ "$CONTEXT" = "staged" ] || [ "$CONTEXT" = "all-changed" ]; then - echo $root_files | xargs -r git add + echo $root_and_packages_pro_files | xargs -r git add fi fi -# Lint Pro files (using Pro's ESLint config) -if [ -n "$pro_files" ]; then +# Lint react_on_rails_pro files (using Pro gem's ESLint config) +if [ -n "$react_on_rails_pro_files" ]; then if [ "$CONTEXT" = "all-changed" ]; then - echo "🔍 ESLint on Pro changed files:" + echo "🔍 ESLint on react_on_rails_pro changed files:" else - echo "🔍 ESLint on Pro $CONTEXT files:" + echo "🔍 ESLint on react_on_rails_pro $CONTEXT files:" fi - printf " %s\n" $pro_files + printf " %s\n" $react_on_rails_pro_files # Strip react_on_rails_pro/ prefix for running in Pro directory - pro_files_relative=$(echo "$pro_files" | sed 's|^react_on_rails_pro/||') + react_on_rails_pro_files_relative=$(echo "$react_on_rails_pro_files" | sed 's|^react_on_rails_pro/||') - if ! (cd react_on_rails_pro && yarn run eslint $pro_files_relative --report-unused-disable-directives --fix); then + if ! (cd react_on_rails_pro && yarn run eslint $react_on_rails_pro_files_relative --report-unused-disable-directives --fix); then exit_code=1 fi # Re-stage files if running on staged or all-changed context if [ "$CONTEXT" = "staged" ] || [ "$CONTEXT" = "all-changed" ]; then - echo $pro_files | xargs -r git add + echo $react_on_rails_pro_files | xargs -r git add fi fi diff --git a/docs/guides/improved-error-messages.md b/docs/guides/improved-error-messages.md new file mode 100644 index 0000000000..25858095fe --- /dev/null +++ b/docs/guides/improved-error-messages.md @@ -0,0 +1,202 @@ +# Improved Error Messages for React on Rails + +React on Rails provides enhanced error messages with actionable solutions to help you quickly identify and fix issues. + +## Smart Error Messages + +React on Rails now provides contextual error messages that: + +- Identify the specific problem +- Suggest concrete solutions with code examples +- Offer similar component names when typos occur +- Prioritize auto-bundling as the recommended approach + +## Auto-Bundling: The Recommended Approach + +React on Rails supports automatic bundling, which eliminates the need for manual component registration. + +### Benefits of Auto-Bundling + +- **No manual registration**: Components are automatically available +- **Simplified development**: Just create the component file and use it +- **Automatic code splitting**: Each component gets its own bundle +- **Better performance**: Only load what you need + +### How to Use Auto-Bundling + +1. **Enable in your view:** + + ```erb + <%= react_component("MyComponent", props: { data: @data }, auto_load_bundle: true) %> + ``` + +2. **Place component in the correct directory:** + + ``` + app/javascript/components/ + └── MyComponent/ + └── MyComponent.jsx # Must export default + ``` + +3. **Generate bundles:** + Bundles are automatically generated during asset precompilation via the Shakapacker precompile hook. For manual generation during development: + ```bash + bundle exec rake react_on_rails:generate_packs + ``` + +That's it! No manual registration needed. + +## Error Message Examples + +### Component Not Registered + +**Before:** + +``` +Component 'HelloWorld' not found +``` + +**After:** + +```` +❌ React on Rails Error + +🔍 Problem: +Component 'HelloWorld' was not found in the component registry. + +💡 Suggested Solution: + +🚀 Recommended: Use Auto-Bundling (No Registration Required!) + +1. Enable auto-bundling in your view: + <%= react_component("HelloWorld", props: {}, auto_load_bundle: true) %> + +2. Place your component in the components directory: + app/javascript/components/HelloWorld/HelloWorld.jsx + + Component structure: + components/ + └── HelloWorld/ + └── HelloWorld.jsx (must export default) + +3. Generate the bundle: + bundle exec rake react_on_rails:generate_packs + +✨ That's it! No manual registration needed. + +───────────────────────────────────────────── + +Alternative: Manual Registration + +If you prefer manual registration: +1. Register in your entry file: + ReactOnRails.register({ HelloWorld: HelloWorld }); + +2. Import the component: + import HelloWorld from './components/HelloWorld'; + +3. Include the bundle in your layout (e.g., `app/views/layouts/application.html.erb`): + ```erb + <%= javascript_pack_tag 'application' %> + <%= stylesheet_pack_tag 'application' %> +```` + +``` + +### Enhanced SSR Errors + +Server-side rendering errors now include: + +- Colored, formatted output for better readability +- Specific error patterns detection (window/document undefined, hydration mismatches) +- Actionable troubleshooting steps +- Props and JavaScript code context +- Console message replay + +**Example SSR Error:** + +``` + +❌ React on Rails Server Rendering Error + +Component: HelloWorldApp + +📋 Error Details: +ReferenceError: window is not defined + +💡 Troubleshooting Suggestions: + +⚠️ Browser API (window/document) accessed during server render + +The component tried to access 'window' which doesn't exist on the server. + +Solutions: +• Wrap browser API calls in useEffect: +useEffect(() => { /_ DOM operations here _/ }, []) + +• Check if running in browser: +if (typeof window !== 'undefined') { /_ browser code _/ } + +• Use dynamic import for browser-only code + +```` + +## Ruby Configuration + +### Using SmartError Directly + +You can create custom smart errors in your Rails code: + +```ruby +raise ReactOnRails::SmartError.new( + error_type: :component_not_registered, + component_name: "MyComponent", + additional_context: { + available_components: ReactOnRails::PackerUtils.registered_components + } +) +```` + +### Error Types + +Available error types: + +- `:component_not_registered` - Component not found in registry +- `:missing_auto_loaded_bundle` - Auto-bundle file not found +- `:hydration_mismatch` - Client/server HTML mismatch +- `:server_render_error` - General SSR error +- `:configuration_error` - Invalid configuration + +## Best Practices + +1. **Prefer auto-bundling** for new components to avoid registration issues +2. **Use server-side rendering** to catch React component errors, hydration mismatches, and SSR-specific issues (like accessing browser APIs) during development before they reach production +3. **Check error messages carefully** - they include specific solutions +4. **Keep components in standard locations** for better error detection + +## Troubleshooting + +If you encounter issues: + +1. **Check component registration:** + + ```bash + bundle exec rake react_on_rails:doctor + ``` + +2. **Verify auto-bundle generation:** + + ```bash + bundle exec rake react_on_rails:generate_packs + ``` + +3. **Enable detailed errors** in development: + ```bash + FULL_TEXT_ERRORS=true rails server + ``` + +## Related Documentation + +- [Auto-Bundling Guide](../core-concepts/auto-bundling-file-system-based-automated-bundle-generation.md) +- [Server Rendering](../core-concepts/react-server-rendering.md) +- [JavaScript API (Component Registration)](../api-reference/javascript-api.md) diff --git a/eslint.config.ts b/eslint.config.ts index b11dc6954a..20a11aaa9b 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -158,14 +158,6 @@ const config = tsEslint.config([ 'import/extensions': ['error', 'ignorePackages'], }, }, - { - files: ['packages/react-on-rails-pro/**/*'], - rules: { - // Disable import/named for pro package - can't resolve monorepo workspace imports - // TypeScript compiler validates these imports - 'import/named': 'off', - }, - }, { files: ['**/*.server.ts', '**/*.server.tsx'], plugins: { @@ -228,6 +220,22 @@ const config = tsEslint.config([ '@typescript-eslint/restrict-template-expressions': 'off', }, }, + { + files: ['packages/react-on-rails-pro/**/*'], + rules: { + // Disable import rules for pro package - can't resolve monorepo workspace imports + // TypeScript compiler validates these imports + 'import/named': 'off', + 'import/no-unresolved': 'off', + // Disable unsafe type rules - Pro package uses internal APIs with complex types + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', + }, + }, { files: ['**/app-react16/**/*'], rules: { diff --git a/lib/generators/react_on_rails/base_generator.rb b/lib/generators/react_on_rails/base_generator.rb index bce4b5de55..8558705088 100644 --- a/lib/generators/react_on_rails/base_generator.rb +++ b/lib/generators/react_on_rails/base_generator.rb @@ -412,8 +412,13 @@ def configure_rspack_in_shakapacker puts Rainbow("🔧 Configuring Shakapacker for Rspack...").yellow # Parse YAML config properly to avoid fragile regex manipulation - config = YAML.load_file(shakapacker_config_path) - + # Support both old and new Psych versions + config = begin + YAML.load_file(shakapacker_config_path, aliases: true) + rescue ArgumentError + # Older Psych versions don't support the aliases parameter + YAML.load_file(shakapacker_config_path) + end # Update default section config["default"] ||= {} config["default"]["assets_bundler"] = "rspack" diff --git a/lib/react_on_rails/helper.rb b/lib/react_on_rails/helper.rb index 9b35da8161..92f4e0c375 100644 --- a/lib/react_on_rails/helper.rb +++ b/lib/react_on_rails/helper.rb @@ -7,6 +7,7 @@ # 1. The white spacing in this file matters! # 2. Keep all #{some_var} fully to the left so that all indentation is done evenly in that var require "react_on_rails/prerender_error" +require "react_on_rails/smart_error" require "addressable/uri" require "react_on_rails/utils" require "react_on_rails/json_output" @@ -638,14 +639,11 @@ def in_mailer? end def raise_missing_autoloaded_bundle(react_component_name) - msg = <<~MSG - **ERROR** ReactOnRails: Component "#{react_component_name}" is configured as "auto_load_bundle: true" - but the generated component entrypoint, which should have been at #{generated_components_pack_path(react_component_name)}, - is missing. You might want to check that this component is in a directory named "#{ReactOnRails.configuration.components_subdirectory}" - & that "bundle exec rake react_on_rails:generate_packs" has been run. - MSG - - raise ReactOnRails::Error, msg + raise ReactOnRails::SmartError.new( + error_type: :missing_auto_loaded_bundle, + component_name: react_component_name, + expected_path: generated_components_pack_path(react_component_name) + ) end end end diff --git a/lib/react_on_rails/prerender_error.rb b/lib/react_on_rails/prerender_error.rb index 5014923adc..8f1196ec62 100644 --- a/lib/react_on_rails/prerender_error.rb +++ b/lib/react_on_rails/prerender_error.rb @@ -47,12 +47,16 @@ def to_error_context private + # rubocop:disable Metrics/AbcSize def calc_message(component_name, console_messages, err, js_code, props) - message = +"ERROR in SERVER PRERENDERING\n" + header = Rainbow("❌ React on Rails Server Rendering Error").red.bright + message = +"#{header}\n\n" + + message << Rainbow("Component: #{component_name}").yellow << "\n\n" + if err + message << Rainbow("Error Details:").red.bright << "\n" message << <<~MSG - Encountered error: - #{err.inspect} MSG @@ -61,33 +65,86 @@ def calc_message(component_name, console_messages, err, js_code, props) err.backtrace.join("\n") else "#{Rails.backtrace_cleaner.clean(err.backtrace).join("\n")}\n" + - Rainbow("The rest of the backtrace is hidden. " \ - "To see the full backtrace, set FULL_TEXT_ERRORS=true.").red + Rainbow("💡 Tip: Set FULL_TEXT_ERRORS=true to see the full backtrace").yellow end else backtrace = nil end - message << <<~MSG - when prerendering #{component_name} with props: #{Utils.smart_trim(props, MAX_ERROR_SNIPPET_TO_LOG)} - - code: - - #{Utils.smart_trim(js_code, MAX_ERROR_SNIPPET_TO_LOG)} - MSG + # Add props information + message << Rainbow("Props:").blue.bright << "\n" + message << "#{Utils.smart_trim(props, MAX_ERROR_SNIPPET_TO_LOG)}\n\n" - if console_messages - message << <<~MSG - console messages: - #{console_messages} - MSG + # Add code snippet + message << Rainbow("JavaScript Code:").blue.bright << "\n" + message << "#{Utils.smart_trim(js_code, MAX_ERROR_SNIPPET_TO_LOG)}\n\n" + if console_messages && console_messages.strip.present? + message << Rainbow("Console Output:").magenta.bright << "\n" + message << "#{console_messages}\n\n" end + # Add actionable suggestions + message << Rainbow("💡 Troubleshooting Steps:").yellow.bright << "\n" + message << build_troubleshooting_suggestions(component_name, err, console_messages) + # Add help and support information message << "\n#{Utils.default_troubleshooting_section}\n" [backtrace, message] + # rubocop:enable Metrics/AbcSize + end + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def build_troubleshooting_suggestions(component_name, err, console_messages) + suggestions = [] + + # Check for common error patterns + if err&.message&.include?("window is not defined") || console_messages&.include?("window is not defined") + suggestions << <<~SUGGESTION + 1. Browser API used on server - wrap with client-side check: + #{Rainbow("if (typeof window !== 'undefined') { ... }").cyan} + SUGGESTION + end + + if err&.message&.include?("document is not defined") || console_messages&.include?("document is not defined") + suggestions << <<~SUGGESTION + 1. DOM API used on server - use React refs or useEffect: + #{Rainbow('useEffect(() => { /* DOM operations here */ }, [])').cyan} + SUGGESTION + end + + if err&.message&.include?("Cannot read") || err&.message&.include?("undefined") + suggestions << <<~SUGGESTION + 1. Check for null/undefined values in props + 2. Add default props or use optional chaining: + #{Rainbow("props.data?.value || 'default'").cyan} + SUGGESTION + end + + if err&.message&.include?("Hydration") || console_messages&.include?("Hydration") + suggestions << <<~SUGGESTION + 1. Server and client render mismatch - ensure consistent: + - Random values (use seed from props) + - Date/time values (pass from server) + - User agent checks (avoid or use props) + SUGGESTION + end + + # Generic suggestions + suggestions << <<~SUGGESTION + • Temporarily disable SSR to isolate the issue: + #{Rainbow('prerender: false').cyan} in your view helper + • Check server logs for detailed errors: + #{Rainbow('tail -f log/development.log').cyan} + • Verify component registration: + #{Rainbow("ReactOnRails.register({ #{component_name}: #{component_name} })").cyan} + • Ensure server bundle is up to date: + #{Rainbow('bin/shakapacker').cyan} or #{Rainbow('yarn run build:server').cyan} + SUGGESTION + + suggestions.join("\n") + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end end end diff --git a/lib/react_on_rails/smart_error.rb b/lib/react_on_rails/smart_error.rb new file mode 100644 index 0000000000..c00a28e583 --- /dev/null +++ b/lib/react_on_rails/smart_error.rb @@ -0,0 +1,326 @@ +# frozen_string_literal: true + +require "rainbow" + +module ReactOnRails + # SmartError provides enhanced error messages with actionable suggestions + # rubocop:disable Metrics/ClassLength + class SmartError < Error + attr_reader :component_name, :error_type, :props, :js_code, :additional_context + + def initialize(error_type:, component_name: nil, props: nil, js_code: nil, **additional_context) + @error_type = error_type + @component_name = component_name + @props = props + @js_code = js_code + @additional_context = additional_context + + message = build_error_message + super(message) + end + + def solution + case error_type + when :component_not_registered + component_not_registered_solution + when :missing_auto_loaded_bundle + missing_auto_loaded_bundle_solution + when :hydration_mismatch + hydration_mismatch_solution + when :server_rendering_error + server_rendering_error_solution + when :redux_store_not_found + redux_store_not_found_solution + when :configuration_error + configuration_error_solution + else + default_solution + end + end + + private + + def build_error_message + header = Rainbow("❌ React on Rails Error: #{error_type_title}").red.bright + + message = <<~MSG + #{header} + + #{error_description} + + #{Rainbow('💡 Suggested Solution:').yellow.bright} + #{solution} + + #{additional_info} + #{troubleshooting_section} + MSG + + message.strip + end + + def error_type_title + case error_type + when :component_not_registered + "Component '#{component_name}' Not Registered" + when :missing_auto_loaded_bundle + "Auto-loaded Bundle Missing" + when :hydration_mismatch + "Hydration Mismatch" + when :server_rendering_error + "Server Rendering Failed" + when :redux_store_not_found + "Redux Store Not Found" + when :configuration_error + "Configuration Error" + else + "Unknown Error" + end + end + + # rubocop:disable Metrics/CyclomaticComplexity + def error_description + case error_type + when :component_not_registered + <<~DESC + Component '#{component_name}' was not found in the component registry. + + React on Rails offers two approaches: + • Auto-bundling (recommended): Components load automatically, no registration needed + • Manual registration: Traditional approach requiring explicit registration + DESC + when :missing_auto_loaded_bundle + <<~DESC + Component '#{component_name}' is configured for auto-loading but its bundle is missing. + Expected location: #{additional_context[:expected_path]} + DESC + when :hydration_mismatch + <<~DESC + The server-rendered HTML doesn't match what React rendered on the client. + Component: #{component_name} + DESC + when :server_rendering_error + <<~DESC + An error occurred while server-side rendering component '#{component_name}'. + #{additional_context[:error_message]} + DESC + when :redux_store_not_found + <<~DESC + Redux store '#{additional_context[:store_name]}' was not found. + Available stores: #{additional_context[:available_stores]&.join(', ') || 'none'} + DESC + when :configuration_error + <<~DESC + Invalid configuration detected. + #{additional_context[:details]} + DESC + else + "An unexpected error occurred." + end + end + + # rubocop:enable Metrics/CyclomaticComplexity + + # rubocop:disable Metrics/AbcSize + def component_not_registered_solution + suggestions = [] + + # Check for similar component names + if component_name && !component_name.empty? + similar = find_similar_components(component_name) + suggestions << "Did you mean one of these? #{similar.map { |s| Rainbow(s).green }.join(', ')}" if similar.any? + end + + suggestions << <<~SOLUTION + #{Rainbow('🚀 Recommended: Use Auto-Bundling (No Registration Required!)').green.bright} + + 1. Enable auto-bundling in your view: + #{Rainbow("<%= react_component(\"#{component_name}\", props: {}, auto_load_bundle: true) %>").cyan} + + 2. Place your component in the components directory: + #{Rainbow("app/javascript/#{ReactOnRails.configuration.components_subdirectory || 'components'}/#{component_name}/#{component_name}.jsx").cyan} + #{' '} + Component structure: + #{Rainbow("#{ReactOnRails.configuration.components_subdirectory || 'components'}/").cyan} + #{Rainbow("└── #{component_name}/").cyan} + #{Rainbow(" └── #{component_name}.jsx").cyan} (must export default) + + 3. Generate the bundle: + #{Rainbow('bundle exec rake react_on_rails:generate_packs').cyan} + + #{Rainbow("✨ That's it! No manual registration needed.").yellow} + + ───────────────────────────────────────────── + + #{Rainbow('Alternative: Manual Registration').gray} + + If you prefer manual registration: + 1. Register in your entry file: + #{Rainbow("ReactOnRails.register({ #{component_name}: #{component_name} });").cyan} + + 2. Import the component: + #{Rainbow("import #{component_name} from './components/#{component_name}';").cyan} + SOLUTION + + suggestions.join("\n") + end + + # rubocop:enable Metrics/AbcSize + def missing_auto_loaded_bundle_solution + <<~SOLUTION + 1. Run the pack generation task: + #{Rainbow('bundle exec rake react_on_rails:generate_packs').cyan} + + 2. Ensure your component is in the correct directory: + #{Rainbow("app/javascript/#{ReactOnRails.configuration.components_subdirectory || 'components'}/#{component_name}/").cyan} + + 3. Check that the component file follows naming conventions: + - Component file: #{Rainbow("#{component_name}.jsx").cyan} or #{Rainbow("#{component_name}.tsx").cyan} + - Must export default + + 4. Verify webpack/shakapacker is configured for nested entries: + #{Rainbow("config.nested_entries_dir = 'components'").cyan} + SOLUTION + end + + def hydration_mismatch_solution + <<~SOLUTION + Common causes and solutions: + + 1. **Random IDs or timestamps**: Use consistent values between server and client + #{Rainbow('// Bad: Math.random() or Date.now()').red} + #{Rainbow('// Good: Use props or deterministic values').green} + + 2. **Browser-only APIs**: Check for client-side before using: + #{Rainbow("if (typeof window !== 'undefined') { ... }").cyan} + + 3. **Different data**: Ensure props are identical on server and client + - Check your redux store initialization + - Verify railsContext is consistent + + 4. **Conditional rendering**: Avoid using user agent or viewport checks + + Debug tips: + - Set #{Rainbow('prerender: false').cyan} temporarily to isolate the issue + - Check browser console for hydration warnings + - Compare server HTML with client render + SOLUTION + end + + def server_rendering_error_solution + <<~SOLUTION + 1. Check your JavaScript console output: + #{Rainbow("tail -f log/development.log | grep 'React on Rails'").cyan} + + 2. Common issues: + - Missing Node.js dependencies: #{Rainbow('cd client && npm install').cyan} + - Syntax errors in component code + - Using browser-only APIs without checks + + 3. Debug server rendering: + - Set #{Rainbow('config.trace = true').cyan} in your configuration + - Set #{Rainbow('config.development_mode = true').cyan} for better errors + - Check #{Rainbow('config.server_bundle_js_file').cyan} points to correct file + + 4. Verify your server bundle: + #{Rainbow('bin/shakapacker').cyan} or #{Rainbow('bin/webpack').cyan} + SOLUTION + end + + def redux_store_not_found_solution + <<~SOLUTION + 1. Register your Redux store: + #{Rainbow("ReactOnRails.registerStore({ #{additional_context[:store_name]}: #{additional_context[:store_name]} });").cyan} + + 2. Ensure the store is imported: + #{Rainbow("import #{additional_context[:store_name]} from './store/#{additional_context[:store_name]}';").cyan} + + 3. Initialize the store before rendering components that depend on it: + #{Rainbow("<%= redux_store('#{additional_context[:store_name]}', props: {}) %>").cyan} + + 4. Check store dependencies in your component: + #{Rainbow("store_dependencies: ['#{additional_context[:store_name]}']").cyan} + SOLUTION + end + + def configuration_error_solution + <<~SOLUTION + Review your React on Rails configuration: + + 1. Check #{Rainbow('config/initializers/react_on_rails.rb').cyan} + + 2. Common configuration issues: + - Invalid bundle paths + - Missing Node modules location + - Incorrect component subdirectory + + 3. Run configuration doctor: + #{Rainbow('rake react_on_rails:doctor').cyan} + SOLUTION + end + + def default_solution + <<~SOLUTION + 1. Check the browser console for JavaScript errors + 2. Review your server logs: #{Rainbow('tail -f log/development.log').cyan} + 3. Run diagnostics: #{Rainbow('rake react_on_rails:doctor').cyan} + 4. Set #{Rainbow('FULL_TEXT_ERRORS=true').cyan} for complete error output + SOLUTION + end + + # rubocop:disable Metrics/AbcSize + def additional_info + info = [] + + info << "#{Rainbow('Component:').blue} #{component_name}" if component_name + + if additional_context[:available_components]&.any? + info << "#{Rainbow('Registered components:').blue} #{additional_context[:available_components].join(', ')}" + end + + info << "#{Rainbow('Rails Environment:').blue} development (detailed errors enabled)" if Rails.env.development? + + info << "#{Rainbow('Auto-load bundles:').blue} enabled" if ReactOnRails.configuration.auto_load_bundle + + return "" if info.empty? + + "\n#{Rainbow('📋 Context:').blue.bright}\n#{info.join("\n")}" + end + + # rubocop:enable Metrics/AbcSize + def troubleshooting_section + "\n#{Rainbow('🔧 Need More Help?').magenta.bright}\n#{Utils.default_troubleshooting_section}" + end + + # rubocop:disable Metrics/CyclomaticComplexity + def find_similar_components(name) + return [] unless additional_context[:available_components] + + available = additional_context[:available_components] + return [] if available.empty? + + available = available.uniq + + # Simple similarity check - could be enhanced with Levenshtein distance + similar = available.select do |comp| + comp.downcase.include?(name.downcase) || name.downcase.include?(comp.downcase) + end + + # Also check for common naming patterns + if similar.empty? + # Check if user forgot to capitalize + capitalized = name.capitalize + similar = available.select { |comp| comp == capitalized } + + # Check for common suffixes + if similar.empty? && !name.end_with?("Component") + with_suffix = "#{name}Component" + similar = available.select { |comp| comp == with_suffix } + end + end + + similar.take(3) # Limit suggestions + # rubocop:enable Metrics/CyclomaticComplexity + end + end + # rubocop:enable Metrics/ClassLength +end diff --git a/packages/react-on-rails-pro/src/ClientSideRenderer.ts b/packages/react-on-rails-pro/src/ClientSideRenderer.ts index 334184e64a..06da1b0733 100644 --- a/packages/react-on-rails-pro/src/ClientSideRenderer.ts +++ b/packages/react-on-rails-pro/src/ClientSideRenderer.ts @@ -188,7 +188,6 @@ You should return a React.Component always for the client side entry point.`); } try { - // eslint-disable-next-line @typescript-eslint/no-deprecated unmountComponentAtNode(domNode); } catch (e: unknown) { const error = e instanceof Error ? e : new Error('Unknown error'); diff --git a/packages/react-on-rails-pro/src/createReactOnRailsPro.ts b/packages/react-on-rails-pro/src/createReactOnRailsPro.ts index 4c0bf488e2..28fe296411 100644 --- a/packages/react-on-rails-pro/src/createReactOnRailsPro.ts +++ b/packages/react-on-rails-pro/src/createReactOnRailsPro.ts @@ -145,13 +145,11 @@ export default function createReactOnRailsPro( if (reactOnRailsPro.streamServerRenderedReactComponent) { reactOnRailsProSpecificFunctions.streamServerRenderedReactComponent = - // eslint-disable-next-line @typescript-eslint/unbound-method reactOnRailsPro.streamServerRenderedReactComponent; } if (reactOnRailsPro.serverRenderRSCReactComponent) { reactOnRailsProSpecificFunctions.serverRenderRSCReactComponent = - // eslint-disable-next-line @typescript-eslint/unbound-method reactOnRailsPro.serverRenderRSCReactComponent; } diff --git a/packages/react-on-rails-pro/tests/SuspenseHydration.test.tsx b/packages/react-on-rails-pro/tests/SuspenseHydration.test.tsx index 187a42f7c5..eaf3822bbb 100644 --- a/packages/react-on-rails-pro/tests/SuspenseHydration.test.tsx +++ b/packages/react-on-rails-pro/tests/SuspenseHydration.test.tsx @@ -128,12 +128,11 @@ async function renderAndHydrate() { }; const writeSecondChunk = async () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment let { done, value } = await reader.read(); let decoded = ''; while (!done) { decoded += new TextDecoder().decode(value as Buffer); - // eslint-disable-next-line no-await-in-loop, @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line no-await-in-loop ({ done, value } = await reader.read()); } diff --git a/packages/react-on-rails/src/base/client.ts b/packages/react-on-rails/src/base/client.ts index c28f02698b..e1debcf7e6 100644 --- a/packages/react-on-rails/src/base/client.ts +++ b/packages/react-on-rails/src/base/client.ts @@ -17,6 +17,8 @@ import createReactOutput from '../createReactOutput.ts'; const DEFAULT_OPTIONS = { traceTurbolinks: false, turbo: false, + debugMode: false, + logComponentRegistration: false, }; interface Registries { @@ -142,6 +144,24 @@ Fix: Use only react-on-rails OR react-on-rails-pro, not both.`); delete newOptions.turbo; } + if (typeof newOptions.debugMode !== 'undefined') { + this.options.debugMode = newOptions.debugMode; + if (newOptions.debugMode) { + console.log('[ReactOnRails] Debug mode enabled'); + } + // eslint-disable-next-line no-param-reassign + delete newOptions.debugMode; + } + + if (typeof newOptions.logComponentRegistration !== 'undefined') { + this.options.logComponentRegistration = newOptions.logComponentRegistration; + if (newOptions.logComponentRegistration) { + console.log('[ReactOnRails] Component registration logging enabled'); + } + // eslint-disable-next-line no-param-reassign + delete newOptions.logComponentRegistration; + } + if (Object.keys(newOptions).length > 0) { throw new Error(`Invalid options passed to ReactOnRails.options: ${JSON.stringify(newOptions)}`); } @@ -164,7 +184,33 @@ Fix: Use only react-on-rails OR react-on-rails-pro, not both.`); // =================================================================== register(components: Record): void { - ComponentRegistry.register(components); + if (this.options.debugMode || this.options.logComponentRegistration) { + // Use performance.now() if available, otherwise fallback to Date.now() + const perf = typeof performance !== 'undefined' ? performance : { now: () => Date.now() }; + const startTime = perf.now(); + const componentNames = Object.keys(components); + console.log( + `[ReactOnRails] Registering ${componentNames.length} component(s): ${componentNames.join(', ')}`, + ); + + ComponentRegistry.register(components); + + const endTime = perf.now(); + console.log( + `[ReactOnRails] Component registration completed in ${(endTime - startTime).toFixed(2)}ms`, + ); + + // Log individual component details if in full debug mode + if (this.options.debugMode) { + componentNames.forEach((name) => { + const component = components[name]; + const size = component.toString().length; + console.log(`[ReactOnRails] ✅ Registered: ${name} (~${(size / 1024).toFixed(1)} chars)`); + }); + } + } else { + ComponentRegistry.register(components); + } }, registerStore(stores: Record): void { diff --git a/packages/react-on-rails/src/types/index.ts b/packages/react-on-rails/src/types/index.ts index c1fc46518a..f2553a1499 100644 --- a/packages/react-on-rails/src/types/index.ts +++ b/packages/react-on-rails/src/types/index.ts @@ -271,6 +271,10 @@ export interface ReactOnRailsOptions { traceTurbolinks?: boolean; /** Turbo (the successor of Turbolinks) events will be registered, if set to true. */ turbo?: boolean; + /** Enable debug mode for detailed logging of React on Rails operations. */ + debugMode?: boolean; + /** Log component registration details including timing and size information. */ + logComponentRegistration?: boolean; } export interface ReactOnRails { diff --git a/spec/react_on_rails/prender_error_spec.rb b/spec/react_on_rails/prender_error_spec.rb index 2975ac2c15..ddb81b7236 100644 --- a/spec/react_on_rails/prender_error_spec.rb +++ b/spec/react_on_rails/prender_error_spec.rb @@ -68,7 +68,7 @@ module ReactOnRails # Ruby version compatibility: match any backtrace reference to the test file backtrace_pattern = /prender_error_spec\.rb:\d+:in ['`]block \(\d+ levels\) in ['`]/ expect(message).to match(backtrace_pattern) - expect(message).to include("The rest of the backtrace is hidden") + expect(message).to include("💡 Tip: Set FULL_TEXT_ERRORS=true to see the full backtrace") end end end diff --git a/spec/react_on_rails/smart_error_spec.rb b/spec/react_on_rails/smart_error_spec.rb new file mode 100644 index 0000000000..f1b35fe06e --- /dev/null +++ b/spec/react_on_rails/smart_error_spec.rb @@ -0,0 +1,185 @@ +# frozen_string_literal: true + +require_relative "spec_helper" + +module ReactOnRails + describe SmartError do + describe "#initialize and #message" do + context "with component_not_registered error" do + subject(:error) do + described_class.new( + error_type: :component_not_registered, + component_name: "ProductCard", + available_components: %w[ProductList ProductDetails UserProfile] + ) + end + + it "creates error with helpful message" do + message = error.message + expect(message).to include("Component 'ProductCard' Not Registered") + expect(message).to include("ReactOnRails.register({ ProductCard: ProductCard })") + expect(message).to include("import ProductCard from './components/ProductCard'") + end + + it "suggests similar components" do + message = error.message + expect(message).to include("ProductList") + expect(message).to include("ProductDetails") + end + + it "includes troubleshooting section" do + message = error.message + expect(message).to include("Get Help & Support") + end + end + + context "with missing_auto_loaded_bundle error" do + subject(:error) do + described_class.new( + error_type: :missing_auto_loaded_bundle, + component_name: "Dashboard", + expected_path: "/app/webpack/generated/Dashboard.js" + ) + end + + it "provides bundle generation guidance" do + message = error.message + expect(message).to include("Auto-loaded Bundle Missing") + expect(message).to include("bundle exec rake react_on_rails:generate_packs") + expect(message).to include("/app/webpack/generated/Dashboard.js") + end + end + + context "with hydration_mismatch error" do + subject(:error) do + described_class.new( + error_type: :hydration_mismatch, + component_name: "UserProfile" + ) + end + + it "provides hydration debugging tips" do + message = error.message + expect(message).to include("Hydration Mismatch") + expect(message).to include("typeof window !== 'undefined'") + expect(message).to include("prerender: false") + expect(message).to include("Use consistent values between server and client") + end + end + + context "with server_rendering_error" do + subject(:error) do + described_class.new( + error_type: :server_rendering_error, + component_name: "ComplexComponent", + error_message: "window is not defined" + ) + end + + it "provides server rendering troubleshooting" do + message = error.message + expect(message).to include("Server Rendering Failed") + expect(message).to include("window is not defined") + expect(message).to include("config.trace = true") + expect(message).to include("bin/shakapacker") + end + end + + context "with redux_store_not_found error" do + subject(:error) do + described_class.new( + error_type: :redux_store_not_found, + store_name: "AppStore", + available_stores: %w[UserStore ProductStore] + ) + end + + it "provides store registration help" do + message = error.message + expect(message).to include("Redux Store Not Found") + expect(message).to include("ReactOnRails.registerStore({ AppStore: AppStore })") + expect(message).to include("UserStore, ProductStore") + end + end + end + + describe "#solution" do + it "returns appropriate solution for each error type" do + errors = [ + { type: :component_not_registered, component: "Test" }, + { type: :missing_auto_loaded_bundle, component: "Test" }, + { type: :hydration_mismatch, component: "Test" }, + { type: :server_rendering_error, component: "Test" }, + { type: :redux_store_not_found, store_name: "TestStore" }, + { type: :configuration_error, details: "Invalid path" } + ] + + errors.each do |error_info| + error = described_class.new( + error_type: error_info[:type], + component_name: error_info[:component], + store_name: error_info[:store_name], + details: error_info[:details] + ) + expect(error.solution).not_to be_empty + expect(error.solution).to be_a(String) + end + end + end + + describe "component name suggestions" do + subject(:error) do + described_class.new( + error_type: :component_not_registered, + component_name: "helloworld", + available_components: %w[HelloWorld HelloWorldApp Header] + ) + end + + it "suggests properly capitalized component names" do + message = error.message + expect(message).to include("HelloWorld") + end + end + + describe "colored output" do + subject(:error) do + described_class.new( + error_type: :component_not_registered, + component_name: "TestComponent" + ) + end + + it "includes colored output markers" do + # Enable Rainbow coloring for this test + Rainbow.enabled = true + message = error.message + # Rainbow adds ANSI color codes + expect(message).to match(/\e\[/) # ANSI escape sequence + ensure + Rainbow.enabled = false + end + end + + describe "context information" do + subject(:error) do + described_class.new( + error_type: :component_not_registered, + component_name: "TestComponent", + available_components: %w[Component1 Component2] + ) + end + + it "includes Rails environment context" do + message = error.message + expect(message).to include("Context:") + expect(message).to include("Component:") + end + + it "shows registered components when available" do + message = error.message + expect(message).to include("Component1, Component2") + end + end + end +end