├── .bowerrc ├── .codeclimate.yml ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── rubocop.yml │ └── ruby.yml ├── .gitignore ├── .pryrc ├── .rubocop.yml ├── .rubocop_todo.yml ├── Appraisals ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE ├── LintingGemfile ├── LintingGemfile.lock ├── README.md ├── Rakefile ├── SECURITY.md ├── VERSIONS.md ├── check_for_uncommitted_files.sh ├── docs ├── common-errors.md ├── component-generator.md ├── controller-actions.md ├── get-started.md ├── migrating-from-react-rails-to-react_on_rails.md ├── server-side-rendering.md ├── ujs.md ├── upgrading.md └── view-helper.md ├── gemfiles ├── base.gemfile ├── base.gemfile.lock ├── shakapacker.gemfile ├── shakapacker.gemfile.lock ├── sprockets_3.gemfile ├── sprockets_3.gemfile.lock ├── sprockets_4.gemfile └── sprockets_4.gemfile.lock ├── lib ├── assets │ ├── javascripts │ │ ├── JSXTransformer.js │ │ └── react_ujs.js │ └── react-source │ │ ├── development │ │ ├── react-server.js │ │ └── react.js │ │ └── production │ │ ├── react-server.js │ │ └── react.js ├── generators │ ├── react │ │ ├── component_generator.rb │ │ └── install_generator.rb │ └── templates │ │ ├── .gitkeep │ │ ├── component.es6.jsx │ │ ├── component.es6.tsx │ │ ├── component.js.jsx │ │ ├── component.js.jsx.coffee │ │ ├── component.js.jsx.tsx │ │ ├── react_server_rendering.rb │ │ ├── server_rendering.js │ │ └── server_rendering_pack.js ├── react-rails.rb ├── react.rb └── react │ ├── jsx.rb │ ├── jsx │ ├── babel_transformer.rb │ ├── jsx_transformer.rb │ ├── processor.rb │ ├── sprockets_strategy.rb │ └── template.rb │ ├── rails.rb │ ├── rails │ ├── asset_variant.rb │ ├── component_mount.rb │ ├── controller_lifecycle.rb │ ├── controller_renderer.rb │ ├── railtie.rb │ ├── test_helper.rb │ ├── version.rb │ └── view_helper.rb │ ├── server_rendering.rb │ └── server_rendering │ ├── bundle_renderer.rb │ ├── bundle_renderer │ ├── console_polyfill.js │ ├── console_replay.js │ ├── console_reset.js │ └── timeout_polyfill.js │ ├── environment_container.rb │ ├── exec_js_renderer.rb │ ├── manifest_container.rb │ ├── separate_server_bundle_container.rb │ └── yaml_manifest_container.rb ├── package.json ├── rakelib └── create_release.rake ├── react-builds ├── package.json ├── react-browser.js ├── react-server.js ├── webpack.config.js └── yarn.lock ├── react-rails.gemspec ├── react_ujs ├── dist │ └── react_ujs.js ├── index.js ├── readme.md ├── src │ ├── events │ │ ├── detect.js │ │ ├── native.js │ │ ├── pjax.js │ │ ├── turbolinks.js │ │ ├── turbolinksClassic.js │ │ └── turbolinksClassicDeprecated.js │ ├── getConstructor │ │ ├── fromGlobal.js │ │ ├── fromRequireContext.js │ │ ├── fromRequireContextWithGlobalFallback.js │ │ └── fromRequireContextsWithGlobalFallback.js │ ├── reactDomClient.js │ ├── renderHelpers.js │ └── supportsRootApi.js └── webpack.config.js ├── test ├── bin │ └── create-fake-js-package-managers ├── dummy │ ├── .gitignore │ ├── .postcssrc.yml │ ├── README.rdoc │ ├── Rakefile │ ├── app │ │ ├── assets │ │ │ ├── config │ │ │ │ └── manifest.js │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ ├── app_no_turbolinks.js │ │ │ │ ├── application.js │ │ │ │ ├── components.js │ │ │ │ ├── components │ │ │ │ │ ├── PlainJSTodo.js │ │ │ │ │ ├── Todo.js.jsx.coffee │ │ │ │ │ ├── TodoList.js.jsx │ │ │ │ │ ├── TodoListWithConsoleLog.js.jsx │ │ │ │ │ └── WithSetTimeout.js.jsx │ │ │ │ ├── example.js.jsx │ │ │ │ ├── example2.js.jsx.coffee │ │ │ │ ├── example3.js.jsx │ │ │ │ ├── flow_types_example.js.jsx │ │ │ │ ├── harmony_example.js.jsx │ │ │ │ ├── pages.js │ │ │ │ ├── require_test │ │ │ │ │ ├── jsx_preprocessor_test.jsx │ │ │ │ │ ├── jsx_require_child_coffee.coffee │ │ │ │ │ ├── jsx_require_child_js.js │ │ │ │ │ └── jsx_require_child_jsx.jsx │ │ │ │ ├── server_rendering.js │ │ │ │ └── turbolinks_only.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── application_controller.rb │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── counters_controller.rb │ │ │ ├── pack_components_controller.rb │ │ │ ├── pages_controller.rb │ │ │ └── server_controller.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── javascript │ │ │ ├── components │ │ │ │ ├── Counter.js │ │ │ │ ├── GreetingMessage.js │ │ │ │ ├── Todo.js │ │ │ │ ├── TodoList.js │ │ │ │ ├── TodoListWithConsoleLog.js │ │ │ │ ├── WithSetTimeout.js │ │ │ │ ├── export_default_component.js │ │ │ │ ├── named_export_component.js │ │ │ │ └── subfolder │ │ │ │ │ └── exports_component.js │ │ │ ├── controllers │ │ │ │ └── mount_counters.js │ │ │ └── packs │ │ │ │ ├── application.js │ │ │ │ └── server_rendering.js │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ └── concerns │ │ │ │ └── .keep │ │ ├── pants │ │ │ └── yfronts.js │ │ └── views │ │ │ ├── counters │ │ │ ├── create.turbo_stream.erb │ │ │ └── index.html.erb │ │ │ ├── layouts │ │ │ ├── app_no_turbolinks.html.erb │ │ │ └── application.html.erb │ │ │ ├── pack_components │ │ │ └── show.html.erb │ │ │ ├── pages │ │ │ ├── _component_with_inner_html.html.erb │ │ │ └── show.html.erb │ │ │ └── server │ │ │ ├── console_example.html.erb │ │ │ ├── console_example_suppressed.html.erb │ │ │ └── show.html.erb │ ├── babel.config.js │ ├── bin │ │ ├── bundle │ │ ├── rails │ │ ├── rake │ │ ├── shakapacker │ │ ├── shakapacker-dev-server │ │ └── yarn │ ├── config.ru │ ├── config │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── environment.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers │ │ │ ├── backtrace_silencers.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ ├── mime_types.rb │ │ │ ├── react.rb │ │ │ ├── secret_token.rb │ │ │ ├── session_store.rb │ │ │ └── wrap_parameters.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── routes.rb │ │ ├── shakapacker.yml │ │ └── webpack │ │ │ ├── clientWebpackConfig.js │ │ │ ├── commonWebpackConfig.js │ │ │ ├── development.js │ │ │ ├── production.js │ │ │ ├── serverClientOrBoth.js │ │ │ ├── serverWebpackConfig.js │ │ │ ├── test.js │ │ │ └── webpack.config.js │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── log │ │ └── .keep │ ├── package.json │ ├── public │ │ ├── 404.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── favicon.ico │ ├── vendor │ │ └── assets │ │ │ ├── javascripts │ │ │ └── .gitkeep │ │ │ └── react │ │ │ ├── JSXTransformer__.js │ │ │ └── test │ │ │ └── react__.js │ └── yarn.lock ├── generators │ ├── coffee_component_generator_test.rb │ ├── component_generator_test.rb │ ├── es6_component_generator_test.rb │ ├── install_generator_sprockets_test.rb │ ├── install_generator_webpacker_test.rb │ └── ts_es6_component_generator_test.rb ├── helper_files │ ├── TodoListWithUpdates.js │ ├── TodoListWithUpdates.js.jsx │ └── WithoutSprockets.js ├── react │ ├── jsx │ │ ├── jsx_prepocessor_test.rb │ │ └── jsx_transformer_test.rb │ ├── jsx_test.rb │ ├── rails │ │ ├── asset_variant_test.rb │ │ ├── component_mount_test.rb │ │ ├── controller_lifecycle_test.rb │ │ ├── pages_controller_test.rb │ │ ├── railtie_test.rb │ │ ├── react_rails_ujs_test.rb │ │ ├── realtime_update_test.rb │ │ ├── test_helper_test.rb │ │ ├── view_helper_test.rb │ │ └── webpacker_test.rb │ ├── server_rendering │ │ ├── bundle_renderer_test.rb │ │ ├── console_replay_test.rb │ │ ├── exec_js_renderer_test.rb │ │ ├── manifest_container_test.rb │ │ ├── webpacker_containers_test.rb │ │ └── yaml_manifest_container_test.rb │ └── server_rendering_test.rb ├── react_asset_test.rb ├── react_test.rb ├── server_rendered_html_test.rb ├── support │ ├── sprockets_helpers.rb │ └── webpacker_helpers.rb └── test_helper.rb └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "vendor/" 3 | } 4 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | exclude_patterns: 3 | - lib/assets/ 4 | - react-builds/ 5 | - react_ujs/dist/ 6 | - test/ 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Help us help you! Have you looked for similar issues? Do you have reproduction steps? [Contributing Guide](CONTRIBUTING.md#reporting-bugs) 2 | 3 | ### Steps to reproduce 4 | 5 | (Guidelines for creating a bug report are [available 6 | here](../CONTRIBUTING.md#reporting-bugs)) 7 | 8 | ### Expected behavior 9 | Tell us what should happen 10 | 11 | ### Actual behavior 12 | Tell us what happens instead 13 | 14 | ### System configuration 15 | - **Shakapacker or Sprockets version**: 16 | - **React-Rails version**: 17 | - **Rect_UJS version**: 18 | - **Rails version**: 19 | - **Ruby version**: 20 | 21 | 22 | ------- 23 | 24 | (Describe your issue here) 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | _Remove this paragraph and provide a general description of the code changes in your pull 4 | request... were there any bugs you had fixed? If so, mention them. If 5 | these bugs have open GitHub issues, be sure to tag them here as well, 6 | to keep the conversation linked together._ 7 | 8 | ### Other Information 9 | 10 | _Remove this paragraph and mention any other important and relevant information such as benchmarks._ 11 | 12 | ### Pull Request checklist 13 | _Remove this line after checking all the items here. If the item is not applicable to the PR, both check it out and wrap it by `~`._ 14 | 15 | - [ ] Add/update test to cover these changes 16 | - [ ] Update documentation 17 | - [ ] Update CHANGELOG file 18 | -------------------------------------------------------------------------------- /.github/workflows/rubocop.yml: -------------------------------------------------------------------------------- 1 | name: Rubocop 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | 9 | jobs: 10 | rubocop: 11 | name: Rubocop 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest] 16 | ruby: ['2.7', '3.0'] 17 | env: 18 | # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps 19 | BUNDLE_GEMFILE: ${{ github.workspace }}/LintingGemfile 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - uses: ruby/setup-ruby@v1 26 | with: 27 | ruby-version: ${{ matrix.ruby }} 28 | bundler-cache: true 29 | - name: Run rubocop 30 | run: bundle exec rubocop 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.log 3 | test/*/tmp 4 | test/*/public/packs 5 | *.swp 6 | /vendor/react 7 | **/node_modules 8 | react-builds/build 9 | coverage/ 10 | **/.yalc/** 11 | yalc.lock 12 | /vendor/bundle 13 | .bundle/config 14 | -------------------------------------------------------------------------------- /.pryrc: -------------------------------------------------------------------------------- 1 | if defined?(PryByebug) 2 | Pry.commands.alias_command 's', 'step' 3 | Pry.commands.alias_command 'n', 'next' 4 | Pry.commands.alias_command 'f', 'finish' 5 | Pry.commands.alias_command 'c', 'continue' 6 | end 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: 4 | - rubocop-performance 5 | - rubocop-minitest 6 | 7 | AllCops: 8 | NewCops: enable 9 | TargetRubyVersion: 2.5 10 | DisplayCopNames: true 11 | 12 | Include: 13 | - '**/Rakefile' 14 | - '**/config.ru' 15 | - 'Gemfile' 16 | - '**/*.rb' 17 | - '**/*.rake' 18 | 19 | Exclude: 20 | <% `git status --ignored --porcelain`.lines.grep(/^!! /).each do |path| %> 21 | - <%= path.sub(/^!! /, '') %> 22 | <% end %> 23 | - '**/*.js' 24 | - '**/node_modules/**/*' 25 | - '**/public/**/*' 26 | - '**/tmp/**/*' 27 | - 'vendor/**/*' 28 | - 'test/dummy_sprockets/**/*' 29 | - 'test/dummy_webpacker1/**/*' 30 | - 'test/dummy_webpacker2/**/*' 31 | - 'test/dummy_webpacker3/**/*' 32 | - 'react_ujs/**/*' 33 | 34 | Naming/FileName: 35 | Exclude: 36 | - '**/Gemfile' 37 | - '**/Rakefile' 38 | - 'lib/react-rails.rb' 39 | 40 | Layout/LineLength: 41 | Max: 120 42 | 43 | Style/StringLiterals: 44 | EnforcedStyle: double_quotes 45 | 46 | Style/Documentation: 47 | Enabled: false 48 | 49 | Style/HashEachMethods: 50 | Enabled: true 51 | 52 | Style/HashTransformKeys: 53 | Enabled: true 54 | 55 | Style/HashTransformValues: 56 | Enabled: true 57 | 58 | Metrics/AbcSize: 59 | Max: 28 60 | 61 | Metrics/CyclomaticComplexity: 62 | Max: 7 63 | 64 | Metrics/PerceivedComplexity: 65 | Max: 10 66 | 67 | Metrics/ClassLength: 68 | Max: 150 69 | 70 | Metrics/ParameterLists: 71 | Max: 5 72 | CountKeywordArgs: false 73 | 74 | Metrics/MethodLength: 75 | Max: 41 76 | 77 | Metrics/ModuleLength: 78 | Max: 180 79 | 80 | Naming/RescuedExceptionsVariableName: 81 | Enabled: false 82 | 83 | # Style/GlobalVars: 84 | # Exclude: 85 | # - 'spec/dummy/config/environments/development.rb' 86 | 87 | Metrics/BlockLength: 88 | Exclude: 89 | - 'test/**/*_test.rb' -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2023-06-30 00:26:13 UTC using RuboCop version 1.53.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | Lint/IneffectiveAccessModifier: 11 | Exclude: 12 | - 'lib/generators/react/component_generator.rb' 13 | 14 | # Offense count: 1 15 | # Configuration parameters: CountComments, CountAsOne. 16 | Metrics/ClassLength: 17 | Exclude: 18 | - 'lib/generators/react/component_generator.rb' 19 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'sprockets_4' do 2 | gem 'sprockets', '~> 4.0.x' 3 | gem 'sprockets-rails' 4 | gem 'turbolinks', '~> 5' 5 | gem 'mini_racer', :platforms => :mri 6 | end 7 | 8 | appraise 'sprockets_3' do 9 | gem 'sprockets', '~> 3.5' 10 | gem 'sprockets-rails' 11 | gem 'turbolinks', '~> 5' 12 | gem 'mini_racer', :platforms => :mri 13 | end 14 | 15 | appraise 'shakapacker' do 16 | gem 'shakapacker', '7.2.0' 17 | end 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neonmd@hotmail.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "http://rubygems.org" 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :minitest do 2 | # with Minitest::Unit 3 | watch(%r{^test/(.*)\/?(.*)_test\.rb$}) 4 | watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[1]}#{m[2]}_test.rb" } 5 | watch(%r{^test/test_helper\.rb$}) { 'test' } 6 | end 7 | -------------------------------------------------------------------------------- /LintingGemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "http://rubygems.org" 4 | # To install gems from this Gemfile locally, use BUNDLE_GEMFILE=./LintingGemfile bundle exec rubocop 5 | gem "rubocop" 6 | gem "rubocop-minitest" 7 | gem "rubocop-performance" 8 | -------------------------------------------------------------------------------- /LintingGemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | ast (2.4.2) 5 | json (2.7.2) 6 | language_server-protocol (3.17.0.3) 7 | parallel (1.24.0) 8 | parser (3.3.1.0) 9 | ast (~> 2.4.1) 10 | racc 11 | racc (1.7.3) 12 | rainbow (3.1.1) 13 | regexp_parser (2.9.0) 14 | rexml (3.2.6) 15 | rubocop (1.63.5) 16 | json (~> 2.3) 17 | language_server-protocol (>= 3.17.0) 18 | parallel (~> 1.10) 19 | parser (>= 3.3.0.2) 20 | rainbow (>= 2.2.2, < 4.0) 21 | regexp_parser (>= 1.8, < 3.0) 22 | rexml (>= 3.2.5, < 4.0) 23 | rubocop-ast (>= 1.31.1, < 2.0) 24 | ruby-progressbar (~> 1.7) 25 | unicode-display_width (>= 2.4.0, < 3.0) 26 | rubocop-ast (1.31.3) 27 | parser (>= 3.3.1.0) 28 | rubocop-minitest (0.35.0) 29 | rubocop (>= 1.61, < 2.0) 30 | rubocop-ast (>= 1.31.1, < 2.0) 31 | rubocop-performance (1.21.0) 32 | rubocop (>= 1.48.1, < 2.0) 33 | rubocop-ast (>= 1.31.1, < 2.0) 34 | ruby-progressbar (1.13.0) 35 | unicode-display_width (2.5.0) 36 | 37 | PLATFORMS 38 | x86_64-darwin-20 39 | x86_64-linux 40 | 41 | DEPENDENCIES 42 | rubocop 43 | rubocop-minitest 44 | rubocop-performance 45 | 46 | BUNDLED WITH 47 | 2.4.9 48 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | begin 4 | require "bundler/setup" 5 | rescue LoadError 6 | puts "You must `gem install bundler` and `bundle install` to run rake tasks" 7 | end 8 | 9 | Bundler::GemHelper.install_tasks 10 | 11 | require "package_json" 12 | 13 | def copy_react_asset(webpack_file, destination_file) 14 | full_webpack_path = File.expand_path("../react-builds/build/#{webpack_file}", __FILE__) 15 | full_destination_path = File.expand_path("../lib/assets/react-source/#{destination_file}", __FILE__) 16 | FileUtils.cp(full_webpack_path, full_destination_path) 17 | end 18 | 19 | namespace :react do 20 | desc "Run the JS build process to put files in the gem source" 21 | task update: %i[install build copy] 22 | 23 | desc "Install the JavaScript dependencies" 24 | task :install do 25 | PackageJson.read("react-builds").manager.install 26 | end 27 | 28 | desc "Build the JS bundles with Webpack" 29 | task :build do 30 | PackageJson.read("react-builds").manager.run("build") 31 | end 32 | 33 | desc "Copy browser-ready JS files to the gem's asset paths" 34 | task :copy do 35 | environments = %w[development production] 36 | environments.each do |environment| 37 | copy_react_asset("#{environment}/react-browser.js", "#{environment}/react.js") 38 | copy_react_asset("#{environment}/react-server.js", "#{environment}/react-server.js") 39 | end 40 | end 41 | end 42 | 43 | namespace :ujs do 44 | desc "Run the JS build process to put files in the gem source" 45 | task update: %i[install build copy] 46 | 47 | desc "Install the JavaScript dependencies" 48 | task :install do 49 | PackageJson.read.manager.install 50 | end 51 | 52 | desc "Build the JS bundles with Webpack" 53 | task :build do 54 | PackageJson.read.manager.run("build") 55 | end 56 | 57 | desc "Copy browser-ready JS files to the gem's asset paths" 58 | task :copy do 59 | full_webpack_path = File.expand_path("react_ujs/dist/react_ujs.js", __dir__) 60 | full_destination_path = File.expand_path("lib/assets/javascripts/react_ujs.js", __dir__) 61 | FileUtils.cp(full_webpack_path, full_destination_path) 62 | end 63 | 64 | desc "Publish the package in ./react_ujs/ to npm as `react_ujs`" 65 | task publish: :update do 66 | `npm publish` 67 | end 68 | end 69 | 70 | require "appraisal" 71 | require "minitest/test_task" 72 | 73 | Minitest::TestTask.create(:test) do |t| 74 | t.libs << "lib" 75 | t.libs << "test" 76 | t.test_globs = ENV["TEST_PATTERN"] || "test/**/*_test.rb" 77 | t.verbose = ENV["TEST_VERBOSE"] == "1" 78 | t.warning = false 79 | end 80 | 81 | task default: :test 82 | 83 | task :test_setup do 84 | Dir.chdir("./test/dummy") do 85 | PackageJson.read.manager.install 86 | end 87 | end 88 | 89 | task test: :test_setup 90 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We support the [latest version](VERSIONS.md) of the project. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | If you discover a security vulnerability, please send an email to 10 | [security@shakacode.com](mailto:security@shakacode.com). We will respond as 11 | quickly as possible to your report. Please do not disclose the 12 | vulnerability publicly until we have had a chance to address it. 13 | 14 | ## Security Measures 15 | 16 | We take security seriously and have implemented the following measures to 17 | protect our project: 18 | 19 | - Regular code reviews 20 | - Automated testing 21 | - Continuous integration and deployment 22 | -------------------------------------------------------------------------------- /VERSIONS.md: -------------------------------------------------------------------------------- 1 | # Versions 2 | 3 | You can control what version of React.js (and JSXTransformer) is used by `react-rails`: 4 | 5 | - Use the [bundled version](#bundled-versions) that comes with the gem 6 | - [Drop in a copy](#drop-in-version) of React.js 7 | 8 | ## Bundled Versions 9 | 10 | | Gem | React.js | | 11 | | -------- | -------- | -------------- | 12 | | master | 16.14.0 | 13 | | 2.6.2 | 16.14.0 | 14 | | 2.6.1 | 16.9.0 | 15 | | 2.6.0 | 16.8.6 | 16 | | 2.5.0 | 16.8.6 | 17 | | 2.4.7 | 16.4.2 | 18 | | 2.4.6 | 16.4.1 | 19 | | 2.4.5 | 16.3.2 | 20 | | 2.4.4 | 16.2.0 | 21 | | 2.4.3 | 16.1.1 | 22 | | 2.4.2 | 16.1.1 | 23 | | 2.4.1 | 16.0.0 | 24 | | 2.4.0 | 16.0.0 | 25 | | 2.3.1 | 15.6.2 | Updated Addons | 26 | | 2.3.0 | 15.6.2 | 27 | | 2.2.1 | 15.4.2 | 28 | | 2.2.0 | 15.4.2 | 29 | | 2.1.0 | 15.4.2 | 30 | | 2.0.2 | 15.4.2 | 31 | | 2.0.0 | 15.4.2 | 32 | | 1.11.0 | 15.4.2 | 33 | | 1.10.0 | 15.4.1 | 34 | | 1.9.0 | 15.3.0 | 35 | | 1.8.2 | 15.3.0 | 36 | | 1.8.1 | 15.2.1 | 37 | | 1.8.0 | 15.0.2 | 38 | | 1.7.2 | 15.0.2 | 39 | | 1.7.1 | 15.0.2 | 40 | | 1.7.0 | 15.0.1 | 41 | | 1.6.2 | 0.14.6 | 42 | | 1.6.1 | 0.14.6 | 43 | | 1.6.0 | 0.14.6 | 44 | | 1.5.0 | 0.14.3 | 45 | | 1.4.2 | 0.14.2 | 46 | | 1.4.1 | 0.14.0 | 47 | | 1.4.0 | 0.14.0 | 48 | | 1.3.3 | 0.13.3 | 49 | | 1.3.2 | 0.13.3 | 50 | | 1.3.1 | 0.13.3 | 51 | | 1.3.0 | 0.13.3 | 52 | | 1.2.0 | 0.13.3 | 53 | | 1.1.0 | 0.13.3 | 54 | | 1.0.0 | ~> 0.13 | 55 | | 0.13.0.0 | 0.13.0 | 56 | | 0.12.2.0 | 0.12.2 | 57 | | 0.12.1.0 | 0.12.1 | 58 | | 0.12.0.0 | 0.12.0 | 59 | 60 | ## Drop-in Version 61 | 62 | You can also provide your own copies of React.js and JSXTransformer. Just add a different version of `react.js` and `react-server.js` from this project or `JSXTransformer.js` (case-sensitive) files to the asset pipeline (eg, `app/assets/vendor/`). 63 | -------------------------------------------------------------------------------- /check_for_uncommitted_files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | status=$(git status --porcelain) 5 | if [ -n "$status" ]; then 6 | status="${status//'%'/'%25'}" 7 | status="${status//$'\n'/'%0A'}" 8 | status="${status//$'\r'/'%0D'}" 9 | echo "$status" 10 | exit 1 11 | else 12 | echo "The repository is clean" 13 | exit 0 14 | fi 15 | -------------------------------------------------------------------------------- /docs/common-errors.md: -------------------------------------------------------------------------------- 1 | # Common Errors 2 | 3 | 4 | 5 | 6 | - [Getting warning for `Can't resolve 'react-dom/client'` in React < 18](#getting-warning-for-cant-resolve-react-domclient-in-react--18) 7 | - [Undefined Set](#undefined-set) 8 | - [Using TheRubyRacer](#using-therubyracer) 9 | - [HMR](#hmr) 10 | - [Tests in component directory](#tests-in-component-directory) 11 | 12 | 13 | 14 | ## Getting warning for `Can't resolve 'react-dom/client'` in React < 18 15 | 16 | You may see a warning like this when building a Webpack bundle using any version of React below 18. This warning can be safely [suppressed](https://webpack.js.org/configuration/other-options/#ignorewarnings) in your Webpack configuration. The following is an example of this suppression in `config/webpack/webpack.config.js`: 17 | 18 | ```diff 19 | - const { webpackConfig } = require('shakapacker') 20 | + const { webpackConfig, merge } = require('shakapacker') 21 | 22 | +const ignoreWarningsConfig = { 23 | + ignoreWarnings: [/Module not found: Error: Can't resolve 'react-dom\/client'/], 24 | +}; 25 | 26 | - module.exports = webpackConfig 27 | + module.exports = merge({}, webpackConfig, ignoreWarningsConfig) 28 | ``` 29 | 30 | ## Undefined Set 31 | ``` 32 | ExecJS::ProgramError (identifier 'Set' undefined): 33 | 34 | (execjs):1 35 | ``` 36 | If you see any variation of this issue, see [Using TheRubyRacer](#using-therubyracer) 37 | 38 | 39 | ## Using TheRubyRacer 40 | TheRubyRacer [hasn't updated LibV8](https://github.com/cowboyd/therubyracer/blob/master/therubyracer.gemspec#L20) (The library that powers Node.js) from v3 in 2 years, any new features are unlikely to work. 41 | 42 | LibV8 itself is already [beyond version 7](https://github.com/cowboyd/libv8/releases/tag/v7.3.492.27.1) therefore many serverside issues are caused by old JS engines and fixed by using an up to date one such as [MiniRacer](https://github.com/discourse/mini_racer) or [TheRubyRhino](https://github.com/cowboyd/therubyrhino) on JRuby. 43 | 44 | ## HMR 45 | 46 | Check out [Enabling Hot Module Replacement (HMR)](https://github.com/shakacode/shakapacker/blob/master/docs/react.md#enabling-hot-module-replacement-hmr) in Shakapacker documentation. 47 | 48 | One caveat is that currently you [cannot Server-Side Render along with HMR](https://github.com/reactjs/react-rails/issues/925#issuecomment-415469572). 49 | 50 | ## Tests in component directory 51 | 52 | If your tests for react components reside alongside the component files in the `app/javascript/components` directory, 53 | you will get `ModuleNotFoundError` in production environment 54 | since test libraries are devDependencies. 55 | 56 | To resolve this issue, 57 | you need to specify a matching pattern in `appllication.js` and `server_rendering.js`. 58 | For example, see the below code: 59 | 60 | ```js 61 | // app/javascript/packs/application.js 62 | const componentRequireContext = require.context('react_rails_components', true, /^(?!.*\.test)^\.\/.*$/) 63 | const ReactRailsUJS = require('react_ujs') 64 | ReactRailsUJS.useContext(componentRequireContext) 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/component-generator.md: -------------------------------------------------------------------------------- 1 | # Component Generator 2 | 3 | 4 | 5 | 6 | - [Use with JBuilder](#use-with-jbuilder) 7 | - [Camelize Props](#camelize-props) 8 | - [Changing Component Templates](#changing-component-templates) 9 | 10 | 11 | 12 | 13 | You can generate a new component file with: 14 | 15 | ```sh 16 | rails g react:component ComponentName prop1:type prop2:type ... [options] 17 | ``` 18 | 19 | For example, 20 | 21 | ```sh 22 | rails g react:component Post title:string published:bool published_by:instanceOf{Person} 23 | ``` 24 | 25 | would generate: 26 | 27 | ```JSX 28 | var Post = createReactClass({ 29 | propTypes: { 30 | title: PropTypes.string, 31 | published: PropTypes.bool, 32 | publishedBy: PropTypes.instanceOf(Person) 33 | }, 34 | 35 | render: function() { 36 | return ( 37 | 38 | Title: {this.props.title} 39 | Published: {this.props.published} 40 | Published By: {this.props.publishedBy} 41 | 42 | ); 43 | } 44 | }); 45 | ``` 46 | 47 | The generator also accepts options: 48 | 49 | - `--es6`: generates a function component 50 | - `--coffee`: use CoffeeScript 51 | 52 | For example, 53 | 54 | ```sh 55 | rails g react:component ButtonComponent title:string --es6 56 | ``` 57 | 58 | would generate: 59 | 60 | ```jsx 61 | import React from "react" 62 | import PropTypes from "prop-types" 63 | 64 | function ButtonComponent(props) { 65 | return ( 66 | 67 | Title: {this.props.title} 68 | 69 | ); 70 | } 71 | 72 | ButtonComponent.propTypes = { 73 | title: PropTypes.string 74 | }; 75 | 76 | export default ButtonComponent 77 | ``` 78 | 79 | **Note:** In a Shakapacker project, es6 template is the default template in the generator. 80 | 81 | Accepted PropTypes are: 82 | 83 | - Plain types: `any`, `array`, `bool`, `element`, `func`, `number`, `object`, `node`, `shape`, `string` 84 | - `instanceOf` takes an optional class name in the form of `instanceOf{className}`. 85 | - `oneOf` behaves like an enum, and takes an optional list of strings in the form of `'name:oneOf{one,two,three}'`. 86 | - `oneOfType` takes an optional list of react and custom types in the form of `'model:oneOfType{string,number,OtherType}'`. 87 | 88 | Note that the arguments for `oneOf` and `oneOfType` must be enclosed in single quotes 89 | to prevent your terminal from expanding them into an argument list. 90 | 91 | ## Use with JBuilder 92 | 93 | If you use Jbuilder to pass a JSON string to `react_component`, make sure your JSON is a stringified hash, 94 | not an array. This is not the Rails default -- you should add the root node yourself. For example: 95 | 96 | ```ruby 97 | # BAD: returns a stringified array 98 | json.array!(@messages) do |message| 99 | json.extract! message, :id, :name 100 | json.url message_url(message, format: :json) 101 | end 102 | 103 | # GOOD: returns a stringified hash 104 | json.messages(@messages) do |message| 105 | json.extract! message, :id, :name 106 | json.url message_url(message, format: :json) 107 | end 108 | ``` 109 | 110 | ## Camelize Props 111 | 112 | You can configure `camelize_props` option: 113 | 114 | ```ruby 115 | MyApp::Application.configure do 116 | config.react.camelize_props = true # default false 117 | end 118 | ``` 119 | 120 | Now, Ruby hashes given to `react_component(...)` as props will have their keys transformed from _underscore_- to _camel_-case, for example: 121 | 122 | ```ruby 123 | { all_todos: @todos, current_status: @status } 124 | # becomes: 125 | { "allTodos" => @todos, "currentStatus" => @status } 126 | ``` 127 | 128 | You can also specify this option in `react_component`: 129 | 130 | ```erb 131 | <%= react_component('HelloMessage', {name: 'John'}, {camelize_props: true}) %> 132 | ``` 133 | 134 | ## Changing Component Templates 135 | 136 | To make simple changes to Component templates, copy the respective template file to your Rails project at `lib/templates/react/component/template_filename`. 137 | 138 | For example, to change the [ES6 Component template](https://github.com/reactjs/react-rails/blob/main/lib/generators/templates/component.es6.jsx), copy it to `lib/templates/react/component/component.es6.jsx` and modify it. 139 | -------------------------------------------------------------------------------- /docs/controller-actions.md: -------------------------------------------------------------------------------- 1 | # Controller Actions 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Components can also be server-rendered directly from a controller action with the custom `component` renderer. For example: 10 | 11 | ```ruby 12 | class TodoController < ApplicationController 13 | def index 14 | @todos = Todo.all 15 | render component: 'TodoList', props: { todos: @todos }, tag: 'span', class: 'todo' 16 | end 17 | end 18 | ``` 19 | 20 | You can also provide the "usual" `render` arguments: `content_type`, `layout`, `location` and `status`. By default, your current layout will be used and the component, rather than a view, will be rendered in place of `yield`. Custom data-* attributes can be passed like `data: {remote: true}`. 21 | 22 | Prerendering is set to `true` by default, but can be turned off with `prerender: false`. 23 | -------------------------------------------------------------------------------- /docs/migrating-from-react-rails-to-react_on_rails.md: -------------------------------------------------------------------------------- 1 | # Migrating from `react-rails` to `react_on_rails` 2 | 3 | 4 | 5 | 6 | - [Why migrate?](#why-migrate) 7 | - [Steps to migrate](#steps-to-migrate) 8 | 9 | 10 | 11 | 12 | ## Why migrate? 13 | 14 | [`react_on_rails`](https://github.com/shakacode/react_on_rails/) offers several additional features for a Rails + React application. The following is a table of features comparison. 15 | 16 | | **Feature** | **react-rails** | **react-on-rails** | 17 | | ----------------------- |:---------------:|:------------------:| 18 | | Sprockets | ✅ | ❌ | 19 | | Shakapacker | ✅ | ✅ | 20 | | SSR | ✅ | ✅ | 21 | | SSR with HMR | ✅ | ✅ | 22 | | SSR with React-Router | ❌ | ✅ | 23 | | SSR with Code Splitting | ❌ | ✅ | 24 | | Node SSR | ❌ | ✅ | 25 | | Advanced Redux support | ❌ | ✅ | 26 | | ReScript support | ❌ | ✅ | 27 | | I18n support | ❌ | ✅ | 28 | 29 | `react_on_rails` offers better performance and bundle optimizations, especially with the option of getting a subscription to `react_on_rails_pro`. 30 | 31 | ## Steps to migrate 32 | 33 | In this guide, it is assumed that you have upgraded the `react-rails` project to use `shakapacker` version 7. To this end, check out [Shakapacker v7 upgrade guide](https://github.com/shakacode/shakapacker/tree/main/docs/v7_upgrade.md). Upgrading `react-rails` to version 3 can make the migration smoother but it is not required. 34 | 35 | 1. Update Deps 36 | 37 | 1. Replace `react-rails` in `Gemfile` with the latest version of `react_on_rails` and run `bundle install`. 38 | 2. Remove `react_ujs` from `package.json` and run `yarn install`. 39 | 3. Commit changes! 40 | 41 | 2. Run `rails g react_on_rails:install` but do not commit the change. `react_on_rails` installs node dependencies and also creates sample react component, Rails view/controller, and update `config/routes.rb`. 42 | 43 | 3. Adapt the project: Check the changes and carefully accept, reject, or modify them as per your project's needs. Besides changes in `config/shakapacker` or `babel.config` which are project-specific, here are the most noticeable changes to address: 44 | 45 | 1. Check webpack config files at `config/webpack/*`. If coming from `react-rails` v3, the changes are minor since you have already made separate configurations for client and server bundles. The most important change here is to notice the different names for the server bundle entry file. You may choose to stick with `server_rendering.js` or use `server-bundle.js` which is the default name in `react_on_rails`. The decision made here, affects the other steps. 46 | 47 | 2. In `app/javascript` directory you may notice some changes. 48 | 49 | 1. `react_on_rails` by default uses `bundles` directory for the React components. You may choose to rename `components` into `bundles` to follow the convention. 50 | 51 | 2. `react_on_rails` uses `client-bundle.js` and `server-bundle.js` instead of `application.js` and `server_rendering.js`. There is nothing special about these names. It can be set to use any other name (as mentioned above). If you too choose to follow the new names, consider updating the relevant `javascript_pack_tag` in your Rails views. 52 | 53 | 3. Update the content of these files to register your React components for client or server-side rendering. Checking the generated files by `react_on_rails` installation process should give enough hints. 54 | 55 | 3. Check Rails views. In `react_on_rails`, `react_component` view helper works slightly differently. It takes two arguments: the component name, and options. Props is one of the options. Take a look at the following example: 56 | 57 | ```diff 58 | - <%= react_component('Post', { title: 'New Post' }, { prerender: true }) %> 59 | + <%= react_component('Post', { props: { title: 'New Post' }, prerender: true }) %> 60 | ``` 61 | 62 | You can also check [react-rails-to-react-on-rails](https://github.com/shakacode/react-rails-example-app/tree/react-rails-to-react-on-rails) branch on [react-rails example app](https://github.com/shakacode/react-rails-example-app) for an example of migration from `react-rails` v3 to `react_on_rails` v13.4. 63 | 64 | -------------------------------------------------------------------------------- /docs/ujs.md: -------------------------------------------------------------------------------- 1 | # UJS 2 | 3 | 4 | 5 | 6 | - [Mounting & Unmounting](#mounting--unmounting) 7 | - [Event Handling](#event-handling) 8 | - [`getConstructor`](#getconstructor) 9 | 10 | 11 | 12 | 13 | `react-rails`'s JavaScript is available as `"react_ujs"` in the asset pipeline or from NPM. It attaches itself to the window as `ReactRailsUJS`. 14 | 15 | ## Mounting & Unmounting 16 | 17 | Usually, `react-rails` mounts & unmounts components automatically as described in [Event Handling](#event-handling) below. 18 | 19 | You can also mount & unmount components from `<%= react_component(...) %>` tags using UJS: 20 | 21 | ```js 22 | // Mount all components on the page: 23 | ReactRailsUJS.mountComponents() 24 | // Mount components within a selector: 25 | ReactRailsUJS.mountComponents(".my-class") 26 | // Mount components within a specific node: 27 | ReactRailsUJS.mountComponents(specificDOMnode) 28 | 29 | // Unmounting works the same way: 30 | ReactRailsUJS.unmountComponents() 31 | ReactRailsUJS.unmountComponents(".my-class") 32 | ReactRailsUJS.unmountComponents(specificDOMnode) 33 | ``` 34 | 35 | You can use this when the DOM is modified by AJAX calls or modal windows. 36 | 37 | ## Event Handling 38 | 39 | `ReactRailsUJS` checks for various libraries to support their page change events: 40 | 41 | - `Turbolinks` 42 | - `pjax` 43 | - `jQuery` 44 | - Native DOM events 45 | 46 | `ReactRailsUJS` will automatically mount components on `<%= react_component(...) %>` tags and unmount them when appropriate. 47 | 48 | If you need to re-detect events, you can call `detectEvents`: 49 | 50 | ```js 51 | // Remove previous event handlers and add new ones: 52 | ReactRailsUJS.detectEvents() 53 | ``` 54 | 55 | For example, if `Turbolinks` is loaded _after_ `ReactRailsUJS`, you'll need to call this again. This function removes previous handlers before adding new ones, so it's safe to call as often as needed. 56 | 57 | If `Turbolinks` is `import`ed via Shakapacker (and thus not available globally), `ReactRailsUJS` will be unable to locate it. To fix this, you can temporarily add it to the global namespace: 58 | 59 | ```js 60 | // Order is particular. First start Turbolinks: 61 | Turbolinks.start(); 62 | // Add Turbolinks to the global namespace: 63 | window.Turbolinks = Turbolinks; 64 | // Remove previous event handlers and add new ones: 65 | ReactRailsUJS.detectEvents(); 66 | // (Optional) Clean up global namespace: 67 | delete window.Turbolinks; 68 | ``` 69 | 70 | ## `getConstructor` 71 | 72 | Components are loaded with `ReactRailsUJS.getConstructor(className)`. This function has two default implementations, depending on if you're using the asset pipeline or Shakapacker: 73 | 74 | - On the asset pipeline, it looks up `className` in the global namespace (`ReactUJS.constructorFromGlobal`). 75 | - On Shakapacker, it `require`s files and accesses named exports, as described in [Use with Shakapacker](./get-started.md#use-with-shakapacker), falling back to the global namespace (`ReactUJS.constructorFromRequireContextWithGlobalFallback`). 76 | 77 | You can override this function to customize the mapping of name-to-constructor. [Server-side rendering](./server-side-rendering.md) also uses this function. 78 | 79 | For example, the fallback behavior of 80 | `ReactUJS.constructorFromRequireContextWithGlobalFallback` can sometimes make 81 | server-side rendering errors hard to debug as it will swallow the original error 82 | (more info 83 | [here](https://github.com/reactjs/react-rails/issues/264#issuecomment-552326663)). 84 | `ReactUJS.constructorFromRequireContext` is provided for this reason. You can 85 | use it like so: 86 | 87 | ```js 88 | // Replaces calls to `ReactUJS.useContext` 89 | ReactUJS.getConstructor = ReactUJS.constructorFromRequireContext(require.context('components', true)); 90 | ``` 91 | 92 | -------------------------------------------------------------------------------- /docs/upgrading.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | 4 | 5 | 6 | - [2.7 to 3.0](#27-to-30) 7 | - [2.3 to 2.4](#23-to-24) 8 | 9 | 10 | 11 | 12 | ## 2.7 to 3.0 13 | - Keep your `react_ujs` up to date: `yarn upgrade` 14 | - **Drop support for Webpacker:** Before any ReactRails upgrade, make sure upgrading from Webpacker to Shakapacker 7. For more information check out Shakapacker 15 | - **SSR:** ReactRails 3.x requires separate compilations for server & client bundles. See [Webpack config](https://github.com/reactjs/react-rails/tree/main/test/dummy/config/webpack) directory in the dummy app to addapt the new implementation. 16 | 17 | ## 2.3 to 2.4 18 | 19 | Keep your `react_ujs` up to date, `yarn upgrade` 20 | 21 | React-Rails 2.4.x uses React 16+ which no longer has React Addons. Therefore the pre-bundled version of react no longer has an addons version, if you need addons still, there is the 2.3.1+ version of the gem that still has addons. 22 | 23 | If you need to make changes in your components for the prebundled react, see the migration docs here: 24 | 25 | - https://reactjs.org/blog/2016/11/16/react-v15.4.0.html 26 | - https://reactjs.org/blog/2017/04/07/react-v15.5.0.html 27 | - https://reactjs.org/blog/2017/06/13/react-v15.6.0.html 28 | 29 | 30 | For the vast majority of cases this will get you most of the migration: 31 | - global find+replace `React.Prop` -> `Prop` 32 | - add `import PropTypes from 'prop-types'` (Webpacker only) 33 | - re-run `bundle exec rails webpacker:install:react` to update npm packages (Webpacker only) 34 | -------------------------------------------------------------------------------- /docs/view-helper.md: -------------------------------------------------------------------------------- 1 | # View Helper 2 | 3 | 4 | 5 | 6 | - [Custom View Helper](#custom-view-helper) 7 | 8 | 9 | 10 | 11 | `react-rails` includes a view helper and an [unobtrusive JavaScript driver](./ujs.md) which work together to put React components on the page. 12 | 13 | The view helper (`react_component`) puts a `div` on the page with the requested component class & props. For example: 14 | 15 | ```erb 16 | <%= react_component('HelloMessage', name: 'John') %> 17 | 18 |
19 | ``` 20 | 21 | On page load, the [`react_ujs` driver](./ujs.md) will scan the page and mount components using `data-react-class` 22 | and `data-react-props`. 23 | 24 | The view helper's signature is: 25 | 26 | ```ruby 27 | react_component(component_class_name, props={}, html_options={}) 28 | ``` 29 | 30 | - `component_class_name` is a string which identifies a component. See [getConstructor](./ujs.md#getconstructor) for details. 31 | - `props` is either: 32 | - an object that responds to `#to_json`; or 33 | - an already-stringified JSON object (see [JBuilder note](./component-generator.md#use-with-jbuilder) below). 34 | - `html_options` may include: 35 | - `tag:` to use an element other than a `div` to embed `data-react-class` and `data-react-props`. 36 | - `prerender: true` to render the component on the server. 37 | - `camelize_props` to [transform a props hash](./component-generator.md#camelize-props) 38 | - `**other` Any other arguments (eg `class:`, `id:`) are passed through to [`content_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag). 39 | 40 | 41 | ## Custom View Helper 42 | 43 | `react-rails` uses a "helper implementation" class to generate the output of the `react_component` helper. The helper is initialized once per request and used for each `react_component` call during that request. You can provide a custom helper class to `config.react.view_helper_implementation`. The class must implement: 44 | 45 | - `#react_component(name, props = {}, options = {}, &block)` to return a string to inject into the Rails view 46 | - `#setup(controller_instance)`, called when the helper is initialized at the start of the request 47 | - `#teardown(controller_instance)`, called at the end of the request 48 | 49 | `react-rails` provides one implementation, `React::Rails::ComponentMount`. 50 | -------------------------------------------------------------------------------- /gemfiles/base.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 7.0.x" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/shakapacker.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "shakapacker", "7.2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/sprockets_3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sprockets", "~> 3.5" 6 | gem "sprockets-rails" 7 | gem "turbolinks", "~> 5" 8 | gem "mini_racer", platforms: :mri 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /gemfiles/sprockets_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "sprockets", "~> 4.0.x" 6 | gem "sprockets-rails" 7 | gem "turbolinks", "~> 5" 8 | gem "mini_racer", platforms: :mri 9 | 10 | gemspec path: "../" 11 | -------------------------------------------------------------------------------- /lib/generators/react/install_generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Generators 5 | class InstallGenerator < ::Rails::Generators::Base 6 | source_root File.expand_path "../templates", __dir__ 7 | 8 | desc "Create default react.js folder layout and prep application.js" 9 | 10 | class_option :skip_git, 11 | type: :boolean, 12 | aliases: "-g", 13 | default: false, 14 | desc: "Skip Git keeps" 15 | 16 | class_option :skip_server_rendering, 17 | type: :boolean, 18 | default: false, 19 | desc: "Don't generate server_rendering.js or config/initializers/react_server_rendering.rb" 20 | 21 | # Make an empty `components/` directory in the right place: 22 | def create_directory 23 | components_dir = if shakapacker? 24 | Pathname.new(javascript_dir).parent.to_s 25 | else 26 | javascript_dir 27 | end 28 | empty_directory File.join(components_dir, "components") 29 | return if options[:skip_git] 30 | 31 | create_file File.join(components_dir, "components/.keep") 32 | end 33 | 34 | # Add requires, setup UJS 35 | def setup_react 36 | if shakapacker? 37 | setup_react_shakapacker 38 | else 39 | setup_react_sprockets 40 | end 41 | end 42 | 43 | def create_server_rendering 44 | if options[:skip_server_rendering] 45 | nil 46 | elsif shakapacker? 47 | ssr_manifest_path = File.join(javascript_dir, "server_rendering.js") 48 | template("server_rendering_pack.js", ssr_manifest_path) 49 | else 50 | ssr_manifest_path = File.join(javascript_dir, "server_rendering.js") 51 | template("server_rendering.js", ssr_manifest_path) 52 | initializer_path = "config/initializers/react_server_rendering.rb" 53 | template("react_server_rendering.rb", initializer_path) 54 | end 55 | end 56 | 57 | private 58 | 59 | def shakapacker? 60 | !!defined?(Shakapacker) 61 | end 62 | 63 | def javascript_dir 64 | if shakapacker? 65 | shakapacker_source_path 66 | .relative_path_from(::Rails.root) 67 | .to_s 68 | else 69 | "app/assets/javascripts" 70 | end 71 | end 72 | 73 | def manifest 74 | Pathname.new(destination_root).join(javascript_dir, "application.js") 75 | end 76 | 77 | def setup_react_sprockets 78 | require_react = "//= require react\n//= require react_ujs\n//= require components\n" 79 | 80 | if manifest.exist? 81 | manifest_contents = File.read(manifest) 82 | 83 | if (match = manifest_contents.match(%r{//=\s+require\s+turbolinks\s+\n})) 84 | inject_into_file manifest, require_react, { after: match[0] } 85 | elsif (match = manifest_contents.match(%r{//=\s+require_tree[^\n]*})) 86 | inject_into_file manifest, require_react, { before: match[0] } 87 | else 88 | append_file manifest, require_react 89 | end 90 | else 91 | create_file manifest, require_react 92 | end 93 | 94 | components_js = "//= require_tree ./components\n" 95 | components_file = File.join(javascript_dir, "components.js") 96 | create_file components_file, components_js 97 | end 98 | 99 | SHAKAPACKER_SETUP_UJS = <<~JS 100 | // Support component names relative to this directory: 101 | var componentRequireContext = require.context("components", true); 102 | var ReactRailsUJS = require("react_ujs"); 103 | ReactRailsUJS.useContext(componentRequireContext); 104 | JS 105 | 106 | def require_package_json_gem 107 | require "bundler/inline" 108 | 109 | gemfile(true) { gem "package_json" } 110 | 111 | puts "using package_json v#{PackageJson::VERSION}" 112 | end 113 | 114 | def setup_react_shakapacker 115 | require_package_json_gem 116 | 117 | PackageJson.read.manager.add(["react_ujs"]) 118 | 119 | if manifest.exist? 120 | append_file(manifest, SHAKAPACKER_SETUP_UJS) 121 | else 122 | create_file(manifest, SHAKAPACKER_SETUP_UJS) 123 | end 124 | end 125 | 126 | def shakapacker_source_path 127 | Shakapacker.config.source_entry_path 128 | end 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/generators/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/lib/generators/templates/.gitkeep -------------------------------------------------------------------------------- /lib/generators/templates/component.es6.jsx: -------------------------------------------------------------------------------- 1 | <%= file_header %> 2 | const <%= component_name %> = (props) => { 3 | return ( 4 | 5 | <% attributes.each do |attribute| -%> 6 | <%= attribute[:name].titleize %>: {props.<%= attribute[:name].camelize(:lower) %>} 7 | <% end -%> 8 | 9 | ) 10 | } 11 | 12 | <% if attributes.size > 0 -%> 13 | <%= file_name.camelize %>.propTypes = { 14 | <% attributes.each_with_index do |attribute, idx| -%> 15 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %> 16 | <% end -%> 17 | }; 18 | <% end -%> 19 | 20 | <%= file_footer %> 21 | -------------------------------------------------------------------------------- /lib/generators/templates/component.es6.tsx: -------------------------------------------------------------------------------- 1 | <%= file_header %> 2 | interface I<%= component_name %>Props { 3 | <% if attributes.size > 0 -%> 4 | <% attributes.each do |attribute| -%> 5 | <% if attribute[:union] -%> 6 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:name].titleize %>; 7 | <% else -%> 8 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %>; 9 | <% end -%> 10 | <% end -%> 11 | <% end -%> 12 | } 13 | 14 | const <%= component_name %> = (props: I<%= component_name %>Props) => { 15 | return ( 16 | 17 | <% attributes.each do |attribute| -%> 18 | <%= attribute[:name].titleize %>: {props.<%= attribute[:name].camelize(:lower) %>} 19 | <% end -%> 20 | 21 | ) 22 | } 23 | 24 | <%= file_footer %> 25 | -------------------------------------------------------------------------------- /lib/generators/templates/component.js.jsx: -------------------------------------------------------------------------------- 1 | <%= file_header %> 2 | function <%= component_name %>(props) { 3 | return ( 4 | 5 | <% attributes.each do |attribute| -%> 6 | <%= attribute[:name].titleize %>: {props.<%= attribute[:name].camelize(:lower) %>} 7 | <% end -%> 8 | 9 | ); 10 | } 11 | 12 | <% if attributes.size > 0 -%> 13 | <%= file_name.camelize %>.propTypes = { 14 | <% attributes.each_with_index do |attribute, idx| -%> 15 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %><% if (idx < attributes.length-1) %>,<% end %> 16 | <% end -%> 17 | }; 18 | <% end -%> 19 | 20 | <%= file_footer %> 21 | -------------------------------------------------------------------------------- /lib/generators/templates/component.js.jsx.coffee: -------------------------------------------------------------------------------- 1 | <%= file_header %>class <%= component_name %> extends React.Component 2 | <% if attributes.size > 0 -%> 3 | @propTypes = 4 | <% attributes.each do |attribute| -%> 5 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %> 6 | <% end -%> 7 | 8 | <% end -%> 9 | render: -> 10 | ` 11 | <% attributes.each do |attribute| -%> 12 | <%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>} 13 | <% end -%> 14 | ` 15 | <%= file_footer %> 16 | -------------------------------------------------------------------------------- /lib/generators/templates/component.js.jsx.tsx: -------------------------------------------------------------------------------- 1 | <%= file_header %> 2 | <% unions = attributes.select{ |a| a[:union] } -%> 3 | <% if unions.size > 0 -%> 4 | <% unions.each do |e| -%> 5 | type <%= e[:name].titleize %> = <%= e[:type]%> 6 | <% end -%> 7 | <% end -%> 8 | 9 | interface I<%= component_name %>Props { 10 | <% if attributes.size > 0 -%> 11 | <% attributes.each do | attribute | -%> 12 | <% if attribute[:union] -%> 13 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:name].titleize %>; 14 | <% else -%> 15 | <%= attribute[:name].camelize(:lower) %>: <%= attribute[:type] %>; 16 | <% end -%> 17 | <% end -%> 18 | <% end -%> 19 | } 20 | 21 | interface I<%= component_name %>State { 22 | } 23 | 24 | class <%= component_name %> extends React.Component Props, I<%= component_name %>State> { 25 | render() { 26 | return ( 27 | 28 | <% attributes.each do |attribute| -%> 29 | <%= attribute[:name].titleize %>: {this.props.<%= attribute[:name].camelize(:lower) %>} 30 | <% end -%> 31 | 32 | ); 33 | } 34 | } 35 | 36 | <%= file_footer %> 37 | -------------------------------------------------------------------------------- /lib/generators/templates/react_server_rendering.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # To render React components in production, precompile the server rendering manifest: 4 | Rails.application.config.assets.precompile += ["server_rendering.js"] 5 | -------------------------------------------------------------------------------- /lib/generators/templates/server_rendering.js: -------------------------------------------------------------------------------- 1 | //= require react-server 2 | //= require react_ujs 3 | //= require ./components 4 | // 5 | // By default, this file is loaded for server-side rendering. 6 | // It should require your components and any dependencies. 7 | -------------------------------------------------------------------------------- /lib/generators/templates/server_rendering_pack.js: -------------------------------------------------------------------------------- 1 | // By default, this pack is loaded for server-side rendering. 2 | // It must expose react_ujs as `ReactRailsUJS` and prepare a require context. 3 | var componentRequireContext = require.context("components", true); 4 | var ReactRailsUJS = require("react_ujs"); 5 | ReactRailsUJS.useContext(componentRequireContext); 6 | -------------------------------------------------------------------------------- /lib/react-rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "react" 4 | require "react/jsx" 5 | require "react/rails" 6 | require "react/server_rendering" 7 | 8 | module React 9 | module Rails 10 | autoload :TestHelper, "react/rails/test_helper" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/react.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | # Recursively camelize `props`, returning a new Hash 5 | # @param props [Object] If it's a Hash or Array, it will be recursed. Otherwise it will be returned. 6 | # @return [Hash] a new hash whose keys are camelized strings 7 | def self.camelize_props(props) 8 | props_as_json = props.as_json 9 | 10 | case props_as_json 11 | when Hash 12 | props_as_json.each_with_object({}) do |(key, value), new_props| 13 | new_key = key.to_s.camelize(:lower) 14 | new_value = camelize_props(value) 15 | new_props[new_key] = new_value 16 | end 17 | when Array 18 | props_as_json.map { |item| camelize_props(item) } 19 | else 20 | props_as_json 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/react/jsx.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "execjs" 4 | require "react/jsx/processor" 5 | require "react/jsx/template" 6 | require "react/jsx/jsx_transformer" 7 | require "react/jsx/babel_transformer" 8 | require "react/jsx/sprockets_strategy" 9 | require "rails" 10 | 11 | module React 12 | module JSX 13 | DEFAULT_TRANSFORMER = BabelTransformer 14 | mattr_accessor :transform_options, :transformer_class, :transformer 15 | 16 | # You can assign `config.react.jsx_transformer_class = ` 17 | # to provide your own transformer. It must implement: 18 | # - #initialize(options) 19 | # - #transform(code) => new code 20 | self.transformer_class = DEFAULT_TRANSFORMER 21 | 22 | # @param code [String] JSX code to transform into JavaScript 23 | # @return [String] plain, browser-ready JavaScript code 24 | def self.transform(code) 25 | self.transformer ||= transformer_class.new(transform_options) 26 | self.transformer.transform(code) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/react/jsx/babel_transformer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "babel/transpiler" 4 | module React 5 | module JSX 6 | # A {React::JSX}-compliant transformer which uses `Babel::Transpiler` to transform JSX. 7 | class BabelTransformer 8 | DEPRECATED_OPTIONS = %i[harmony strip_types asset_path].freeze 9 | DEFAULT_TRANSFORM_OPTIONS = { blacklist: ["spec.functionName", "validation.react", "strict"] }.freeze 10 | def initialize(options) 11 | if (options.keys & DEPRECATED_OPTIONS).any? 12 | ActiveSupport::Deprecation.warn( 13 | <<-MSG 14 | Setting config.react.jsx_transform_options for :harmony, :strip_types, and :asset_path keys is now deprecated and has no effect with the default Babel Transformer. 15 | Please use new Babel Transformer options :whitelist, :plugin instead. 16 | MSG 17 | ) 18 | end 19 | 20 | @transform_options = DEFAULT_TRANSFORM_OPTIONS.merge(options) 21 | end 22 | 23 | def transform(code) 24 | Babel::Transpiler.transform(code, @transform_options)["code"] 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/react/jsx/jsx_transformer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module JSX 5 | # A {React::JSX}-compliant transformer which uses the deprecated `JSXTransformer.js` to transform JSX. 6 | class JSXTransformer 7 | DEFAULT_ASSET_PATH = "JSXTransformer.js" 8 | 9 | def initialize(options) 10 | @transform_options = { 11 | stripTypes: options.fetch(:strip_types, false), 12 | harmony: options.fetch(:harmony, false) 13 | } 14 | 15 | @asset_path = options.fetch(:asset_path, DEFAULT_ASSET_PATH) 16 | 17 | # If execjs uses therubyracer, there is no 'global'. Make sure 18 | # we have it so JSX script can work properly. 19 | js_code = "var global = global || this;#{jsx_transform_code}" 20 | @context = ExecJS.compile(js_code) 21 | end 22 | 23 | def transform(code) 24 | result = @context.call("JSXTransformer.transform", code, @transform_options) 25 | result["code"] 26 | end 27 | 28 | # search for transformer file using sprockets - allows user to override 29 | # this file in their own application 30 | def jsx_transform_code 31 | ::Rails.application.assets[@asset_path].to_s 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/react/jsx/processor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module JSX 5 | # A Sprockets 3+-compliant processor 6 | class Processor 7 | def self.call(input) 8 | JSX.transform(input[:data]) 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/react/jsx/sprockets_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module JSX 5 | # Depending on the Sprockets version, 6 | # attach JSX transformation the the Sprockets environment. 7 | # 8 | # You can override it with `config.sprockets_strategy` 9 | # @example Specifying a Sprockets strategy 10 | # app.config.react.sprockets_strategy = :register_engine 11 | # 12 | # @example Opting out of any Sprockets strategy 13 | # app.config.react.sprockets_strategy = false 14 | # 15 | module SprocketsStrategy 16 | module_function 17 | 18 | # @param [Sprockets::Environment] the environment to attach JSX to 19 | # @param [Symbol, Nil] A strategy name, or `nil` to detect a strategy 20 | def attach_with_strategy(sprockets_env, strategy_or_nil) 21 | strategy = strategy_or_nil || detect_strategy 22 | public_send(strategy, sprockets_env) 23 | end 24 | 25 | # @return [Symbol] based on the environment, return a method name to call with the sprockets environment 26 | def detect_strategy 27 | sprockets_version = Gem::Version.new(Sprockets::VERSION) 28 | if sprockets_version >= Gem::Version.new("4.a") 29 | :register_processors 30 | elsif sprockets_version >= Gem::Version.new("3.0.0") 31 | :register_engine_with_mime_type 32 | else 33 | :register_engine 34 | end 35 | end 36 | 37 | def register_engine(sprockets_env) 38 | sprockets_env.register_engine(".jsx", React::JSX::Template) 39 | end 40 | 41 | def register_engine_with_mime_type(sprockets_env) 42 | sprockets_env.register_engine(".jsx", React::JSX::Processor, mime_type: "application/javascript", 43 | silence_deprecation: true) 44 | end 45 | 46 | def register_processors(sprockets_env) 47 | sprockets_env.register_mime_type("application/jsx", extensions: [".jsx", ".js.jsx", ".es.jsx", ".es6.jsx"]) 48 | sprockets_env.register_mime_type("application/jsx+coffee", extensions: [".jsx.coffee", ".js.jsx.coffee"]) 49 | sprockets_env.register_transformer("application/jsx", "application/javascript", React::JSX::Processor) 50 | sprockets_env.register_transformer("application/jsx+coffee", "application/jsx", 51 | Sprockets::CoffeeScriptProcessor) 52 | sprockets_env.register_preprocessor("application/jsx", 53 | Sprockets::DirectiveProcessor.new(comments: ["//", ["/*", "*/"]])) 54 | sprockets_env.register_preprocessor("application/jsx+coffee", 55 | Sprockets::DirectiveProcessor.new(comments: ["#", ["###", "###"]])) 56 | end 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/react/jsx/template.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "tilt" 4 | 5 | module React 6 | module JSX 7 | # Sprockets 2-compliant processor 8 | class Template < Tilt::Template 9 | self.default_mime_type = "application/javascript" 10 | 11 | def prepare; end 12 | 13 | def evaluate(_scope, _locals) 14 | @evaluate ||= JSX.transform(data) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/react/rails.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "react/rails/asset_variant" 4 | require "react/rails/railtie" 5 | require "react/rails/controller_lifecycle" 6 | require "react/rails/version" 7 | require "react/rails/component_mount" 8 | require "react/rails/view_helper" 9 | require "react/rails/controller_renderer" 10 | -------------------------------------------------------------------------------- /lib/react/rails/asset_variant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | # This class accepts some options for which build you want, then exposes where you can find 6 | # them. In general, these paths should be added to the sprockets environment. 7 | class AssetVariant 8 | GEM_ROOT = Pathname.new("../../../../").expand_path(__FILE__) 9 | # @return [String] "production" or "development" 10 | attr_reader :react_build 11 | 12 | # @return [String] The path which contains the specified React.js build as "react.js" 13 | attr_reader :react_directory 14 | 15 | # @return [String] The path which contains the JSX Transformer 16 | attr_reader :jsx_directory 17 | 18 | # @param [Hash] Options for the asset variant 19 | # @option variant [Symbol] if `:production`, use the minified React.js build 20 | def initialize(options = {}) 21 | @react_build = options[:variant] == :production ? "production" : "development" 22 | 23 | @react_directory = GEM_ROOT.join("lib/assets/react-source/").join(@react_build).to_s 24 | @jsx_directory = GEM_ROOT.join("lib/assets/javascripts/").to_s 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/react/rails/component_mount.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | # This is the default view helper implementation. 6 | # It just inserts HTML into the DOM (see {#react_component}). 7 | # 8 | # You can extend this class or provide your own implementation 9 | # by assigning it to `config.react.view_helper_implementation`. 10 | class ComponentMount 11 | include ActionView::Helpers::TagHelper 12 | include ActionView::Helpers::TextHelper 13 | attr_accessor :output_buffer 14 | 15 | mattr_accessor :camelize_props_switch 16 | 17 | def initialize 18 | @cache_ids = [] 19 | end 20 | 21 | # {ControllerLifecycle} calls these hooks 22 | # You can use them in custom helper implementations 23 | def setup(controller) 24 | @controller = controller 25 | end 26 | 27 | def teardown(controller); end 28 | 29 | # Render a UJS-type HTML tag annotated with data attributes, which 30 | # are used by react_ujs to actually instantiate the React component 31 | # on the client. 32 | def react_component(name, props = {}, options = {}, &block) 33 | options = { tag: options } if options.is_a?(Symbol) 34 | props = React.camelize_props(props) if options.fetch(:camelize_props, camelize_props_switch) 35 | 36 | prerender_options = options[:prerender] 37 | block = proc { concat(prerender_component(name, props, prerender_options)) } if prerender_options 38 | 39 | html_options = generate_html_options(name, options, props, prerender_options) 40 | 41 | rendered_tag(html_options, &block) 42 | end 43 | 44 | private 45 | 46 | # If this controller has checked out a renderer, use that one. 47 | # Otherwise, use {React::ServerRendering} directly (which will check one out for this rendering). 48 | def prerender_component(component_name, props, prerender_options) 49 | renderer = @controller.try(:react_rails_prerenderer) || React::ServerRendering 50 | renderer.render(component_name, props, prerender_options) 51 | end 52 | 53 | def generate_html_options(name, options, props, prerender_options) 54 | html_options = options.reverse_merge(data: {}) 55 | 56 | unless prerender_options == :static 57 | html_options[:data].tap do |data| 58 | data[:react_class] = name 59 | data[:react_props] = (props.is_a?(String) ? props : props.to_json) 60 | data[:hydrate] = "t" if prerender_options 61 | 62 | num_components = @cache_ids.count { |c| c.start_with? name } 63 | data[:react_cache_id] = "#{name}-#{num_components}" 64 | end 65 | end 66 | 67 | html_options 68 | end 69 | 70 | def rendered_tag(html_options, &block) 71 | html_tag = html_options[:tag] || :div 72 | 73 | # remove internally used properties so they aren't rendered to DOM 74 | html_option_to_use = html_options.except(:tag, :prerender, :camelize_props) 75 | 76 | tag = content_tag(html_tag, "", html_option_to_use, &block) 77 | return tag unless React::ServerRendering.renderer_options[:replay_console] 78 | 79 | # Grab the server-rendered console replay script 80 | # and move it _outside_ the container div 81 | tag.sub!(%r{\n()$}m, '\1') 82 | tag.html_safe 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/react/rails/controller_lifecycle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | # This module is included into ActionController so that 6 | # per-request hooks can be called in the view helper. 7 | module ControllerLifecycle 8 | extend ActiveSupport::Concern 9 | 10 | included do 11 | # use both names to support Rails 3..5 12 | around_action_with_fallback = respond_to?(:around_action) ? :around_action : :around_filter 13 | public_send(around_action_with_fallback, :use_react_component_helper) 14 | attr_reader :__react_component_helper 15 | end 16 | 17 | module ClassMethods 18 | # Call this in the controller to check out a prerender for the whole request. 19 | # You can access the renderer with {#react_rails_prerenderer}. 20 | def per_request_react_rails_prerenderer 21 | around_action_with_fallback = respond_to?(:around_action) ? :around_action : :around_filter 22 | public_send(around_action_with_fallback, :per_request_react_rails_prerenderer) 23 | end 24 | end 25 | 26 | # Instantiate the ViewHelper implementation and call its #setup method 27 | # then let the controller action run, 28 | # then call the ViewHelper implementation's #teardown method 29 | def use_react_component_helper 30 | new_helper = React::Rails::ViewHelper.helper_implementation_class.new 31 | new_helper.setup(self) 32 | @__react_component_helper = new_helper 33 | yield 34 | @__react_component_helper.teardown(self) 35 | end 36 | 37 | # If you want a per-request renderer, add this method as an around-action 38 | # 39 | # (`.per_request_react_rails_prerenderer` does this for you) 40 | # @example Having one renderer instance for each controller action 41 | # around_action :per_request_react_rails_prerenderer 42 | def per_request_react_rails_prerenderer 43 | React::ServerRendering.with_renderer do |renderer| 44 | @__react_rails_prerenderer = renderer 45 | yield 46 | end 47 | end 48 | 49 | # An instance of a server renderer, for use during this request 50 | def react_rails_prerenderer 51 | @__react_rails_prerenderer 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/react/rails/controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | # A renderer class suitable for `ActionController::Renderers`. 6 | # It is associated to `:component` in the Railtie. 7 | # 8 | # It is prerendered by default with {React::ServerRendering}. 9 | # Set options[:prerender] to `false` to disable prerendering. 10 | # 11 | # @example Rendering a component from a controller 12 | # class TodosController < ApplicationController 13 | # def index 14 | # @todos = Todo.all 15 | # render component: 'TodoList', props: { todos: @todos }, tag: 'span', class: 'todo' 16 | # end 17 | # end 18 | class ControllerRenderer 19 | include React::Rails::ViewHelper 20 | include ActionView::Helpers::TagHelper 21 | include ActionView::Helpers::TextHelper 22 | 23 | attr_accessor :output_buffer 24 | 25 | def initialize(options = {}) 26 | controller = options[:controller] 27 | @__react_component_helper = controller.__react_component_helper 28 | end 29 | 30 | # @return [String] HTML for `component_name` with `options[:props]` 31 | def call(component_name, options, &block) 32 | props = options.fetch(:props, {}) 33 | options = default_options.merge(options.slice(:data, :aria, :tag, :class, :id, :prerender, :camelize_props)) 34 | react_component(component_name, props, options, &block) 35 | end 36 | 37 | private 38 | 39 | def default_options 40 | { prerender: true } 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/react/rails/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | module TestHelper 6 | extend ActiveSupport::Concern 7 | 8 | # assert react_component render 9 | # 10 | # assert_react_component("HelloWorld") do |props| 11 | # assert_equal "Hello world", props[:message] 12 | # end 13 | def assert_react_component(name) 14 | assert_select "div[data-react-class=?]", name do |dom| 15 | if block_given? 16 | props = JSON.parse(dom.attr("data-react-props")) 17 | props.deep_symbolize_keys! 18 | 19 | yield(props) 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/react/rails/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | # If you change this, make sure to update VERSIONS.md 6 | # and republish the UJS by updating package.json and `bundle exec rake ujs:publish` 7 | VERSION = "3.2.1" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/react/rails/view_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module Rails 5 | module ViewHelper 6 | # This class will be used for inserting tags into HTML. 7 | # It should implement: 8 | # - #setup(controller_instance) 9 | # - #teardown(controller_instance) 10 | # - #react_component(name, props, options &block) 11 | # The default is {React::Rails::ComponentMount} 12 | mattr_accessor :helper_implementation_class 13 | 14 | # Render a React component into the view 15 | # using the {helper_implementation_class} 16 | # 17 | # If called during a Rails controller-managed request, use the instance 18 | # created by the controller. 19 | # 20 | # Otherwise, make a new instance. 21 | def react_component(*args, &block) 22 | helper_obj = @__react_component_helper ||= helper_implementation_class.new 23 | helper_obj.react_component(*args) { capture(&block) if block } 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/react/server_rendering.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "connection_pool" 4 | require "react/server_rendering/exec_js_renderer" 5 | require "react/server_rendering/bundle_renderer" 6 | 7 | module React 8 | module ServerRendering 9 | mattr_accessor :renderer, :renderer_options, 10 | :pool_size, :pool_timeout 11 | 12 | self.renderer_options = {} 13 | 14 | # Discard the old ConnectionPool & create a new one. 15 | # This will clear all state such as loaded code, JS VM state, or options. 16 | # @return [void] 17 | def self.reset_pool 18 | options = { size: pool_size, timeout: pool_timeout } 19 | @pool = ConnectionPool.new(options) { renderer.new(renderer_options) } 20 | end 21 | 22 | # Check a renderer out of the pool and use it to render the component. 23 | # @param component_name [String] Component identifier, looked up by UJS 24 | # @param props [String, Hash] Props for this component 25 | # @param prerender_options [Hash] Renderer-specific options 26 | # @return [String] Prerendered HTML from `component_name` 27 | def self.render(component_name, props, prerender_options) 28 | @pool.with do |renderer| 29 | renderer.render(component_name, props, prerender_options) 30 | end 31 | end 32 | 33 | # Yield a renderer for an arbitrary block 34 | def self.with_renderer(&block) 35 | @pool.with(&block) 36 | end 37 | 38 | # Raised when something went wrong during server rendering. 39 | class PrerenderError < RuntimeError 40 | def initialize(component_name, props, js_message) 41 | message = ["Encountered error \"#{js_message.inspect}\" when prerendering #{component_name} with #{props}", 42 | js_message.backtrace.join("\n")].join("\n") 43 | super(message) 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/react/server_rendering/bundle_renderer/console_polyfill.js: -------------------------------------------------------------------------------- 1 | var console = { history: [] }; 2 | ['error', 'log', 'info', 'warn'].forEach(function (fn) { 3 | console[fn] = function () { 4 | console.history.push({level: fn, arguments: Array.prototype.slice.call(arguments)}); 5 | }; 6 | }); 7 | -------------------------------------------------------------------------------- /lib/react/server_rendering/bundle_renderer/console_replay.js: -------------------------------------------------------------------------------- 1 | (function (history) { 2 | if (history && history.length > 0) { 3 | result += '\n'; 4 | history.forEach(function (msg) { 5 | result += '\nconsole.' + msg.level + '.apply(console, ' + JSON.stringify(msg.arguments) + ');'; 6 | }); 7 | result += '\n'; 8 | } 9 | })(console.history); 10 | -------------------------------------------------------------------------------- /lib/react/server_rendering/bundle_renderer/console_reset.js: -------------------------------------------------------------------------------- 1 | if (typeof console !== "undefined" && console.history) { 2 | console.history = []; 3 | } 4 | -------------------------------------------------------------------------------- /lib/react/server_rendering/bundle_renderer/timeout_polyfill.js: -------------------------------------------------------------------------------- 1 | function getStackTrace() { 2 | var stack; 3 | try { 4 | throw new Error(''); 5 | } 6 | catch (error) { 7 | stack = error.stack || ''; 8 | } 9 | stack = stack.split('\\n').map(function (line) { 10 | return line.trim(); 11 | }); 12 | return stack.splice(stack[0] == 'Error' ? 2 : 1); 13 | }; 14 | 15 | function printError(functionName){ 16 | console.error(functionName + ' is not defined for execJS. See https://github.com/sstephenson/execjs#faq. Note babel-polyfill may call this.'); 17 | console.error(getStackTrace().join('\\n')); 18 | }; 19 | 20 | function setTimeout() { 21 | printError('setTimeout'); 22 | }; 23 | 24 | function clearTimeout() { 25 | printError('clearTimeout'); 26 | }; 27 | -------------------------------------------------------------------------------- /lib/react/server_rendering/environment_container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module ServerRendering 5 | # Return asset contents by getting them from a Sprockets::Environment instance. 6 | # 7 | # This is good for Rails development but bad for production because: 8 | # - It compiles the asset lazily, not ahead-of-time 9 | # - Rails 5 / Sprockets 3 doesn't expose a Sprockets::Environment in production. 10 | class EnvironmentContainer 11 | def initialize 12 | @environment = ::Rails.application.assets 13 | end 14 | 15 | def find_asset(logical_path) 16 | @environment[logical_path].to_s 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/react/server_rendering/exec_js_renderer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module ServerRendering 5 | # A bare-bones renderer for React.js + Exec.js 6 | # - Depends on global ReactRailsUJS in the ExecJS context 7 | # - No Rails dependency 8 | # - No browser concerns 9 | class ExecJSRenderer 10 | # @return [ExecJS::Runtime::Context] The JS context for this renderer 11 | attr_reader :context 12 | 13 | def initialize(options = {}) 14 | js_code = options[:code] || raise("Pass `code:` option to instantiate a JS context!") 15 | full_code = GLOBAL_WRAPPER + js_code 16 | # File.write("./test/dummy/tmp/latest_js_context.js", full_code) 17 | @context = ExecJS.compile(full_code) 18 | end 19 | 20 | def render(component_name, props, prerender_options) 21 | js_executed_before = before_render(component_name, props, prerender_options) 22 | js_executed_after = after_render(component_name, props, prerender_options) 23 | js_main_section = main_render(component_name, props, prerender_options) 24 | render_from_parts(js_executed_before, js_main_section, js_executed_after) 25 | rescue ExecJS::ProgramError => err 26 | raise React::ServerRendering::PrerenderError.new(component_name, props, err) 27 | end 28 | 29 | # Hooks for inserting JS before/after rendering 30 | def before_render(_component_name, _props, _prerender_options) 31 | "" 32 | end 33 | 34 | def after_render(_component_name, _props, _prerender_options) 35 | "" 36 | end 37 | 38 | # Handle Node.js & other ExecJS contexts 39 | GLOBAL_WRAPPER = <<-JS 40 | var global = global || this; 41 | var self = self || this; 42 | JS 43 | 44 | private 45 | 46 | def render_from_parts(before, main, after) 47 | js_code = compose_js(before, main, after) 48 | @context.eval(js_code).html_safe 49 | end 50 | 51 | def main_render(component_name, props, prerender_options) 52 | render_function = prerender_options.fetch(:render_function, "renderToString") 53 | "this.ReactRailsUJS.serverRender('#{render_function}', '#{component_name}', #{props})" 54 | end 55 | 56 | def compose_js(before, main, after) 57 | <<-JS 58 | (function () { 59 | #{before} 60 | var result = #{main}; 61 | #{after} 62 | return result; 63 | })() 64 | JS 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/react/server_rendering/manifest_container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module ServerRendering 5 | # Get asset content by reading the compiled file from disk using a Sprockets::Manifest. 6 | # 7 | # This is good for Rails production when assets are compiled to public/assets 8 | # but sometimes, they're compiled to other directories (or other servers) 9 | class ManifestContainer 10 | def initialize 11 | @manifest = ::Rails.application.assets_manifest 12 | end 13 | 14 | def find_asset(logical_path) 15 | asset_path = @manifest.assets[logical_path] || raise( 16 | "No compiled asset for #{logical_path}, was it precompiled?" 17 | ) 18 | asset_full_path = ::Rails.root.join("public", @manifest.dir, asset_path) 19 | File.read(asset_full_path) 20 | end 21 | 22 | # sprockets-rails < 2.2.2 does not have `application.assets_manifest` 23 | def self.compatible? 24 | ::Rails.application.respond_to?(:assets_manifest) 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/react/server_rendering/separate_server_bundle_container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "open-uri" 4 | 5 | module React 6 | module ServerRendering 7 | # Get a compiled file from Shakapacker's output path 8 | class SeparateServerBundleContainer 9 | def self.compatible? 10 | !!defined?(Shakapacker) 11 | end 12 | 13 | def find_asset(filename) 14 | asset_path = Shakapacker.config.public_output_path.join(filename).to_s 15 | File.read(asset_path) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/react/server_rendering/yaml_manifest_container.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module React 4 | module ServerRendering 5 | # Get asset content by reading the compiled file from disk using the generated manifest.yml file 6 | # 7 | # This is good for Rails production when assets are compiled to public/assets 8 | # but sometimes, they're compiled to other directories (or other servers) 9 | class YamlManifestContainer 10 | def initialize 11 | @assets = YAML.load_file(public_asset_path("manifest.yml")) 12 | end 13 | 14 | def find_asset(logical_path) 15 | asset_path = @assets[logical_path] || raise("No compiled asset for #{logical_path}, was it precompiled?") 16 | File.read(public_asset_path(asset_path)) 17 | end 18 | 19 | def self.compatible? 20 | ::Rails::VERSION::MAJOR == 3 21 | end 22 | 23 | private 24 | 25 | def public_asset_path(asset_name) 26 | asset_path = File.join("public", ::Rails.application.config.assets.prefix, asset_name) 27 | ::Rails.root.join(asset_path) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react_ujs", 3 | "version": "3.2.1", 4 | "description": "Rails UJS for the react-rails gem", 5 | "repository": "reactjs/react-rails", 6 | "main": "react_ujs/index.js", 7 | "files": [ 8 | "react_ujs" 9 | ], 10 | "scripts": { 11 | "build": "cd react_ujs && webpack" 12 | }, 13 | "dependencies": { 14 | "@babel/preset-react": "^7.22.5", 15 | "css-loader": "^6.8.1", 16 | "css-minimizer-webpack-plugin": "^5.0.1", 17 | "mini-css-extract-plugin": "^2.7.6", 18 | "prop-types": "^15.8.1", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "react_ujs": "^2.7.1", 22 | "style-loader": "^3.3.3" 23 | }, 24 | "devDependencies": { 25 | "webpack": "^5.74.0", 26 | "webpack-cli": "^5.0.1" 27 | }, 28 | "packageManager": "yarn@1.22.21" 29 | } 30 | -------------------------------------------------------------------------------- /react-builds/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-rails-builds", 3 | "version": "0.0.0", 4 | "description": "Prepares react-rails asset files", 5 | "main": "react.js", 6 | "scripts": { 7 | "build": "NODE_ENV=development webpack && NODE_ENV=production webpack" 8 | }, 9 | "dependencies": { 10 | "create-react-class": "^15.6.2", 11 | "fast-text-encoding": "^1.0.6", 12 | "immutability-helper": "^2.4.0", 13 | "node-polyfill-webpack-plugin": "^2.0.1", 14 | "prop-types": "^15.6.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "webpack": "^5.74.0" 18 | }, 19 | "packageManager": "yarn@1.22.21" 20 | } 21 | -------------------------------------------------------------------------------- /react-builds/react-browser.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var ReactDOM = require("react-dom"); 3 | var createReactClass = require("create-react-class"); 4 | var PropTypes = require("prop-types"); 5 | 6 | window.React = React; 7 | window.ReactDOM = ReactDOM; 8 | window.createReactClass = createReactClass; 9 | window.PropTypes = PropTypes; 10 | -------------------------------------------------------------------------------- /react-builds/react-server.js: -------------------------------------------------------------------------------- 1 | // polyfill TextEncoder & TextDecoder onto `util` b/c `node-util` polyfill doesn't include them 2 | // https://github.com/browserify/node-util/issues/46 3 | import util from 'util'; 4 | import 'fast-text-encoding'; 5 | 6 | Object.assign(util, { TextDecoder, TextEncoder }); 7 | 8 | var React = require("react"); 9 | var ReactDOMServer = require("react-dom/server"); 10 | var createReactClass = require("create-react-class"); 11 | var PropTypes = require("prop-types"); 12 | 13 | global.React = React; 14 | global.ReactDOMServer = ReactDOMServer; 15 | global.createReactClass = createReactClass; 16 | global.PropTypes = PropTypes; 17 | -------------------------------------------------------------------------------- /react-builds/webpack.config.js: -------------------------------------------------------------------------------- 1 | // Use `rake react:update` to build this bundle & copy files into the gem. 2 | // Be sure to set NODE_ENV=production or NODE_ENV=development before running 3 | const NodePolyfillPlugin = require('node-polyfill-webpack-plugin'); 4 | 5 | module.exports = { 6 | context: __dirname, 7 | entry: { 8 | "react-browser": "./react-browser.js", 9 | "react-server": "./react-server.js", 10 | }, 11 | output: { 12 | path: __dirname + "/build/" + process.env.NODE_ENV, 13 | filename: "[name].js", 14 | }, 15 | plugins: [ 16 | new NodePolyfillPlugin({ 17 | excludeAliases: ['console', 'Buffer'], 18 | }), 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /react-rails.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $:.push File.expand_path('../lib', __FILE__) 4 | require 'react/rails/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'react-rails' 8 | s.version = React::Rails::VERSION 9 | s.summary = 'React integration for Ruby on Rails' 10 | s.description = 'Render components in views or controller actions. Server-side rendering powered by ExecJS. Transform JSX in the asset pipeline or use Shakapacker.' 11 | s.homepage = 'https://github.com/reactjs/react-rails' 12 | s.license = 'Apache-2.0' 13 | 14 | s.author = ['Paul O’Shannessy', 'Robert Mosolgo', 'Gregory Myers', 'Tsukuru Tanimichi'] 15 | s.email = ['paul@oshannessy.com', 'rmosolgo@gmail.com', 'neonmd@hotmail.co.uk', 'info@ttanimichi.com'] 16 | 17 | s.add_development_dependency 'appraisal' 18 | s.add_development_dependency 'bundler', '2.4.9' 19 | s.add_development_dependency 'codeclimate-test-reporter' 20 | s.add_development_dependency 'coffee-rails' 21 | s.add_development_dependency 'es5-shim-rails', '>= 2.0.5' 22 | s.add_development_dependency 'gem-release' 23 | s.add_development_dependency 'guard' 24 | s.add_development_dependency 'guard-minitest' 25 | s.add_development_dependency 'jbuilder' 26 | s.add_development_dependency 'listen', '~> 3.0.0' 27 | s.add_development_dependency 'capybara' 28 | s.add_development_dependency 'selenium-webdriver' 29 | s.add_development_dependency 'test-unit', '~> 2.5' 30 | s.add_development_dependency 'pry-byebug' 31 | s.add_development_dependency 'package_json' 32 | s.add_development_dependency 'rails', '~> 7.0.7', '>= 7.0.7.2' 33 | s.add_development_dependency 'turbo-rails' 34 | s.add_development_dependency 'minitest-retry' 35 | 36 | s.add_dependency 'connection_pool' 37 | s.add_dependency 'execjs' 38 | s.add_dependency 'railties', '>= 3.2' 39 | s.add_dependency 'tilt' 40 | s.add_dependency 'babel-transpiler', '>=0.7.0' 41 | 42 | s.files = Dir[ 43 | 'lib/**/*', 44 | 'README.md', 45 | 'CHANGELOG.md', 46 | 'LICENSE' 47 | ] 48 | 49 | s.require_paths = ['lib'] 50 | end 51 | -------------------------------------------------------------------------------- /react_ujs/readme.md: -------------------------------------------------------------------------------- 1 | # react-rails UJS 2 | 3 | UJS driver for [`react-rails`](https://github.com/reactjs/react-rails). See the Ruby gem for license, documentation and changelog. 4 | -------------------------------------------------------------------------------- /react_ujs/src/events/detect.js: -------------------------------------------------------------------------------- 1 | var nativeEvents = require("./native") 2 | var pjaxEvents = require("./pjax") 3 | var turbolinksEvents = require("./turbolinks") 4 | var turbolinksClassicDeprecatedEvents = require("./turbolinksClassicDeprecated") 5 | var turbolinksClassicEvents = require("./turbolinksClassic") 6 | 7 | // see what things are globally available 8 | // and setup event handlers to those things 9 | module.exports = function(ujs) { 10 | if (ujs.handleEvent) { 11 | // We're calling this a second time -- remove previous handlers 12 | if (typeof Turbolinks !== "undefined" && typeof Turbolinks.EVENTS !== "undefined") { 13 | turbolinksClassicEvents.teardown(ujs); 14 | } 15 | turbolinksEvents.teardown(ujs); 16 | turbolinksClassicDeprecatedEvents.teardown(ujs); 17 | pjaxEvents.teardown(ujs); 18 | nativeEvents.teardown(ujs); 19 | } 20 | 21 | if ('addEventListener' in window) { 22 | ujs.handleEvent = function(eventName, callback) { 23 | document.addEventListener(eventName, callback); 24 | }; 25 | ujs.removeEvent = function(eventName, callback) { 26 | document.removeEventListener(eventName, callback); 27 | }; 28 | } else { 29 | ujs.handleEvent = function(eventName, callback) { 30 | window.attachEvent(eventName, callback); 31 | }; 32 | ujs.removeEvent = function(eventName, callback) { 33 | window.detachEvent(eventName, callback); 34 | }; 35 | } 36 | 37 | // Detect which kind of events to set up: 38 | if (typeof Turbolinks !== 'undefined' && Turbolinks.supported) { 39 | if (typeof Turbolinks.EVENTS !== 'undefined') { 40 | // Turbolinks.EVENTS is in classic version 2.4.0+ 41 | turbolinksClassicEvents.setup(ujs) 42 | } else if (typeof Turbolinks.controller !== "undefined") { 43 | // Turbolinks.controller is in version 5+ 44 | turbolinksEvents.setup(ujs); 45 | } else { 46 | turbolinksClassicDeprecatedEvents.setup(ujs); 47 | } 48 | } else if (typeof $ !== "undefined" && typeof $.pjax === 'function') { 49 | pjaxEvents.setup(ujs); 50 | } else { 51 | nativeEvents.setup(ujs); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /react_ujs/src/events/native.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Attach handlers to browser events to mount 3 | // (There are no unmount handlers since the page is destroyed on navigation) 4 | setup: function(ujs) { 5 | if ('addEventListener' in window) { 6 | ujs.handleEvent('DOMContentLoaded', ujs.handleMount); 7 | } else { 8 | // add support to IE8 without jQuery 9 | ujs.handleEvent('onload', ujs.handleMount); 10 | } 11 | }, 12 | 13 | teardown: function(ujs) { 14 | ujs.removeEvent('DOMContentLoaded', ujs.handleMount); 15 | ujs.removeEvent('onload', ujs.handleMount); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /react_ujs/src/events/pjax.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // pjax support 3 | setup: function(ujs) { 4 | ujs.handleEvent('ready', ujs.handleMount); 5 | ujs.handleEvent('pjax:end', ujs.handleMount); 6 | ujs.handleEvent('pjax:beforeReplace', ujs.handleUnmount); 7 | }, 8 | 9 | teardown: function(ujs) { 10 | ujs.removeEvent('ready', ujs.handleMount); 11 | ujs.removeEvent('pjax:end', ujs.handleMount); 12 | ujs.removeEvent('pjax:beforeReplace', ujs.handleUnmount); 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /react_ujs/src/events/turbolinks.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Turbolinks 5+ got rid of named events (?!) 3 | setup: function(ujs) { 4 | ujs.handleEvent('turbolinks:load', ujs.handleMount); 5 | }, 6 | 7 | teardown: function(ujs) { 8 | ujs.removeEvent('turbolinks:load', ujs.handleMount); 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /react_ujs/src/events/turbolinksClassic.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Attach handlers to Turbolinks-Classic events 3 | // for mounting and unmounting components 4 | setup: function(ujs) { 5 | ujs.handleEvent(Turbolinks.EVENTS.CHANGE, ujs.handleMount); 6 | ujs.handleEvent(Turbolinks.EVENTS.BEFORE_UNLOAD, ujs.handleUnmount); 7 | }, 8 | teardown: function(ujs) { 9 | ujs.removeEvent(Turbolinks.EVENTS.CHANGE, ujs.handleMount); 10 | ujs.removeEvent(Turbolinks.EVENTS.BEFORE_UNLOAD, ujs.handleUnmount); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /react_ujs/src/events/turbolinksClassicDeprecated.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Before Turbolinks 2.4.0, Turbolinks didn't 3 | // have named events and didn't have a before-unload event. 4 | // Also, it didn't work with the Turbolinks cache, see 5 | // https://github.com/reactjs/react-rails/issues/87 6 | setup: function(ujs) { 7 | Turbolinks.pagesCached(0) 8 | ujs.handleEvent('page:change', ujs.handleMount); 9 | ujs.handleEvent('page:receive', ujs.handleUnmount); 10 | }, 11 | teardown: function(ujs) { 12 | ujs.removeEvent('page:change', ujs.handleMount); 13 | ujs.removeEvent('page:receive', ujs.handleUnmount); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /react_ujs/src/getConstructor/fromGlobal.js: -------------------------------------------------------------------------------- 1 | // Assume className is simple and can be found at top-level (window). 2 | // Fallback to eval to handle cases like 'My.React.ComponentName'. 3 | // Also, try to gracefully import Babel 6 style default exports 4 | var topLevel = typeof window === "undefined" ? this : window; 5 | 6 | module.exports = function(className) { 7 | var constructor; 8 | // Try to access the class globally first 9 | constructor = topLevel[className]; 10 | 11 | // If that didn't work, try eval 12 | if (!constructor) { 13 | constructor = eval(className); 14 | } 15 | 16 | // Lastly, if there is a default attribute try that 17 | if (constructor && constructor['default']) { 18 | constructor = constructor['default']; 19 | } 20 | 21 | return constructor; 22 | } 23 | -------------------------------------------------------------------------------- /react_ujs/src/getConstructor/fromRequireContext.js: -------------------------------------------------------------------------------- 1 | // Load React components by requiring them from "components/", for example: 2 | // 3 | // - "pages/index" -> `require("components/pages/index")` 4 | // - "pages/show.Header" -> `require("components/pages/show").Header` 5 | // - "pages/show.Body.Content" -> `require("components/pages/show").Body.Content` 6 | // 7 | module.exports = function(reqctx) { 8 | return function(className) { 9 | var parts = className.split(".") 10 | var filename = parts.shift() 11 | var keys = parts 12 | // Load the module: 13 | var component = reqctx("./" + filename) 14 | // Then access each key: 15 | keys.forEach(function(k) { 16 | component = component[k] 17 | }) 18 | // support `export default` 19 | if (component.__esModule) { 20 | component = component["default"] 21 | } 22 | return component 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /react_ujs/src/getConstructor/fromRequireContextWithGlobalFallback.js: -------------------------------------------------------------------------------- 1 | // Make a function which: 2 | // - First tries to require the name 3 | // - Then falls back to global lookup 4 | var fromGlobal = require("./fromGlobal") 5 | var fromRequireContext = require("./fromRequireContext") 6 | 7 | module.exports = function(reqctx) { 8 | var fromCtx = fromRequireContext(reqctx) 9 | return function(className) { 10 | var component; 11 | try { 12 | // `require` will raise an error if this className isn't found: 13 | component = fromCtx(className) 14 | } catch (firstErr) { 15 | // fallback to global: 16 | try { 17 | component = fromGlobal(className) 18 | } catch (secondErr) { 19 | console.error(firstErr) 20 | console.error(secondErr) 21 | } 22 | } 23 | return component 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /react_ujs/src/getConstructor/fromRequireContextsWithGlobalFallback.js: -------------------------------------------------------------------------------- 1 | // Make a function which: 2 | // - First tries to require the name 3 | // - Then falls back to global lookup 4 | var fromGlobal = require("./fromGlobal") 5 | var fromRequireContext = require("./fromRequireContext") 6 | 7 | module.exports = function(reqctxs) { 8 | var fromCtxs = reqctxs.map((reqctx) => fromRequireContext(reqctx)) 9 | return function(className) { 10 | var component; 11 | try { 12 | var index = 0, fromCtx, firstErr; 13 | do { 14 | fromCtx = fromCtxs[index]; 15 | 16 | try { 17 | // `require` will raise an error if this className isn't found: 18 | component = fromCtx(className) 19 | } catch (fromCtxErr) { 20 | if (!firstErr) { 21 | firstErr = fromCtxErr; 22 | } 23 | } 24 | 25 | index += 1; 26 | } while (index < fromCtxs.length); 27 | if (!component) throw firstErr; 28 | } catch (firstErr) { 29 | // fallback to global: 30 | try { 31 | component = fromGlobal(className) 32 | } catch (secondErr) { 33 | console.error(firstErr) 34 | console.error(secondErr) 35 | } 36 | } 37 | return component 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /react_ujs/src/reactDomClient.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom" 2 | import supportsRootApi from "./supportsRootApi" 3 | 4 | let reactDomClient = ReactDOM 5 | 6 | if (supportsRootApi) { 7 | // This will never throw an exception, but it's the way to tell Webpack the dependency is optional 8 | // https://github.com/webpack/webpack/issues/339#issuecomment-47739112 9 | // Unfortunately, it only converts the error to a warning. 10 | try { 11 | // eslint-disable-next-line global-require,import/no-unresolved 12 | reactDomClient = require('react-dom/client'); 13 | } catch (e) { 14 | // We should never get here, but if we do, we'll just use the default ReactDOM 15 | // and live with the warning. 16 | reactDomClient = ReactDOM; 17 | } 18 | } 19 | 20 | export default reactDomClient 21 | -------------------------------------------------------------------------------- /react_ujs/src/renderHelpers.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from "./reactDomClient" 2 | import supportsRootApi from "./supportsRootApi" 3 | 4 | export function supportsHydration() { 5 | return typeof ReactDOM.hydrate === "function" || typeof ReactDOM.hydrateRoot === "function" 6 | } 7 | 8 | export function reactHydrate(node, component) { 9 | if (typeof ReactDOM.hydrateRoot === "function") { 10 | return ReactDOM.hydrateRoot(node, component) 11 | } else { 12 | return ReactDOM.hydrate(component, node) 13 | } 14 | } 15 | 16 | export function createReactRootLike(node) { 17 | if(supportsRootApi) { 18 | return ReactDOM.createRoot(node) 19 | } 20 | return legacyReactRootLike(node) 21 | } 22 | 23 | function legacyReactRootLike(node) { 24 | const root = { 25 | render(component) { 26 | return ReactDOM.render(component, node) 27 | } 28 | } 29 | return root 30 | } 31 | -------------------------------------------------------------------------------- /react_ujs/src/supportsRootApi.js: -------------------------------------------------------------------------------- 1 | var ReactDOM = require("react-dom") 2 | 3 | var reactMajorVersion, supportsRootApi; 4 | if (typeof ReactDOM != "undefined") { 5 | reactMajorVersion = ReactDOM.version.split('.')[0] || 16 6 | 7 | // TODO: once we require React 18, we can remove this and inline everything guarded by it. 8 | supportsRootApi = reactMajorVersion >= 18 9 | } else { 10 | supportsRootApi = false 11 | } 12 | 13 | module.exports = supportsRootApi 14 | -------------------------------------------------------------------------------- /react_ujs/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname, 3 | entry: "./index.js", 4 | output: { 5 | path: __dirname + "/dist/", 6 | filename: "react_ujs.js", 7 | library: "ReactRailsUJS", 8 | libraryTarget: 'umd' 9 | }, 10 | externals: { 11 | 'react': { 12 | root: 'React', 13 | commonjs2: 'react', 14 | commonjs: 'react', 15 | amd: 'react' 16 | }, 17 | 'react-dom': { 18 | root: 'ReactDOM', 19 | commonjs2: 'react-dom', 20 | commonjs: 'react-dom', 21 | amd: 'react-dom' 22 | }, 23 | 'react-dom/server': { 24 | root: 'ReactDOMServer', 25 | commonjs2: 'react-dom/server', 26 | commonjs: 'react-dom/server', 27 | amd: 'react-dom/server' 28 | } 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /test/bin/create-fake-js-package-managers: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # creates a set of fake JavaScript package managers in a temporary bin 5 | # directory for GitHub Actions, _excluding_ the one passed in as an 6 | # argument in order to assert that only that package manager is used 7 | 8 | require "tmpdir" 9 | 10 | # setup the bin directory we want to use 11 | bin_dir = Dir.mktmpdir("react-rails-") 12 | 13 | if ENV["GITHUB_ACTIONS"] 14 | puts "adding #{bin_dir} to GITHUB_PATH..." 15 | File.write(ENV.fetch("GITHUB_PATH"), "#{bin_dir}\n", mode: "a+") 16 | end 17 | 18 | managers = %w[npm yarn pnpm bun] 19 | manager_in_use = ARGV[0] 20 | 21 | Dir.chdir(bin_dir) do 22 | managers.each do |manager| 23 | next if manager == manager_in_use 24 | 25 | puts "creating #{bin_dir}/#{manager}..." 26 | File.write( 27 | manager, 28 | <<~CONTENTS 29 | #!/usr/bin/env node 30 | 31 | throw new Error("(#{manager}) this is not the package manager you're looking..."); 32 | CONTENTS 33 | ) 34 | File.chmod(0o755, manager) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/dummy/.gitignore: -------------------------------------------------------------------------------- 1 | /public/packs 2 | /node_modules 3 | /public/packs 4 | /public/packs-test 5 | /node_modules 6 | -------------------------------------------------------------------------------- /test/dummy/.postcssrc.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | postcss-smart-import: {} 3 | postcss-cssnext: {} 4 | -------------------------------------------------------------------------------- /test/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Add your own tasks in files placed in lib/tasks ending in .rake, 4 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 5 | 6 | require File.expand_path("config/application", __dir__) 7 | 8 | Dummy::Application.load_tasks 9 | -------------------------------------------------------------------------------- /test/dummy/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | // Sprockets 4 expects this file 2 | // 3 | //= link application.js 4 | //= link turbolinks_only.js 5 | //= link application.css 6 | //= link app_no_turbolinks.js -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/app_no_turbolinks.js: -------------------------------------------------------------------------------- 1 | //= require react 2 | //= require react_ujs 3 | //= require_tree ./components 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | // 14 | // es5-shim is necessary for PhantomJS to pass tests. See https://github.com/facebook/react/issues/303 15 | // 16 | //= require turbolinks 17 | //= require es5-shim/es5-shim 18 | //= require react 19 | //= require react_ujs 20 | //= require_tree ./components 21 | //= require ./pages 22 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components.js: -------------------------------------------------------------------------------- 1 | //= require_self 2 | //= require_tree ./components 3 | //= require ./pages 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/PlainJSTodo.js: -------------------------------------------------------------------------------- 1 | var Todo = createReactClass({ 2 | render: function() { 3 | return React.createElement("li", null, this.props.todo) 4 | } 5 | }) 6 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/Todo.js.jsx.coffee: -------------------------------------------------------------------------------- 1 | Todo = createReactClass 2 | render: -> 3 | `
  • {this.props.todo}
  • ` 4 | 5 | # Because Coffee files are in an anonymous function, 6 | # expose it for server rendering tests 7 | this.Todo = Todo 8 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/TodoList.js.jsx: -------------------------------------------------------------------------------- 1 | TodoList = createReactClass({ 2 | getInitialState: function() { 3 | return({mounted: "nope"}); 4 | }, 5 | componentDidMount: function() { 6 | this.setState({mounted: 'yep'}); 7 | }, 8 | render: function() { 9 | console.log("Test Console Replay") 10 | return ( 11 | 17 | ) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/TodoListWithConsoleLog.js.jsx: -------------------------------------------------------------------------------- 1 | TodoListWithConsoleLog = createReactClass({ 2 | getInitialState: function() { 3 | console.log('got initial state'); 4 | return({mounted: "nope"}); 5 | }, 6 | componentWillMount: function() { 7 | console.warn('mounted component'); 8 | this.setState({mounted: 'yep'}); 9 | }, 10 | render: function() { 11 | var x = 'foo'; 12 | console.error('rendered!', x); 13 | return ( 14 | 21 | ) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/components/WithSetTimeout.js.jsx: -------------------------------------------------------------------------------- 1 | WithSetTimeout = createReactClass({ 2 | componentWillMount: function () { 3 | setTimeout(function () {}, 1000) 4 | clearTimeout(0) 5 | }, 6 | render: function () { 7 | return I am rendered! 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/example.js.jsx: -------------------------------------------------------------------------------- 1 | [2, ...[1]]; 2 |
    ; 3 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/example2.js.jsx.coffee: -------------------------------------------------------------------------------- 1 | Component = createReactClass 2 | render: -> 3 | `` 4 | 5 | this.Component = Component 6 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/example3.js.jsx: -------------------------------------------------------------------------------- 1 |
    ; 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/flow_types_example.js.jsx: -------------------------------------------------------------------------------- 1 | function flowTypesExample(i: number, name: string): string { 2 | return "OK" 3 | } -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/harmony_example.js.jsx: -------------------------------------------------------------------------------- 1 | var HarmonyComponent = createReactClass({ 2 | statics: { 3 | generateGreeting() { 4 | return "Hello Harmony!" 5 | }, 6 | generateGreetingWithWrapper() { 7 | var insertedGreeting = this.generateGreeting(); 8 | return `Your greeting is: '${insertedGreeting}'.` 9 | }, 10 | }, 11 | render: function(){ 12 | var greeting = HarmonyComponent.generateGreeting(); 13 | var { active, ...other } = { active: true, x: 1, y:2 } 14 | return ( 15 |
    16 |

    {greeting}

    17 |
    18 | 19 |
    20 |
    21 | ) 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/pages.js: -------------------------------------------------------------------------------- 1 | var GreetingMessage = createReactClass({ 2 | getInitialState: function() { 3 | var initialGreeting = 'Hello'; 4 | if (typeof global !== "undefined" && global.ctx && global.ctx.greeting) { 5 | initialGreeting = global.ctx.greeting 6 | } 7 | 8 | return { 9 | greeting: initialGreeting 10 | } 11 | }, 12 | goodbye: function() { 13 | this.setState({greeting: 'Goodbye'}); 14 | }, 15 | render: function() { 16 | return React.createElement('div', {}, 17 | React.createElement('div', {}, this.state.greeting, ' ', this.props.name ), 18 | React.createElement('button', {onClick: this.goodbye}, 'Goodbye') 19 | ); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/require_test/jsx_preprocessor_test.jsx: -------------------------------------------------------------------------------- 1 | //= require ./jsx_require_child_jsx 2 | //= require ./jsx_require_child_js 3 | //= require ./jsx_require_child_coffee 4 |
    5 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/require_test/jsx_require_child_coffee.coffee: -------------------------------------------------------------------------------- 1 | requireCoffee = true 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/require_test/jsx_require_child_js.js: -------------------------------------------------------------------------------- 1 | var requirePlainJavascript = true; 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/require_test/jsx_require_child_jsx.jsx: -------------------------------------------------------------------------------- 1 |
    2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/server_rendering.js: -------------------------------------------------------------------------------- 1 | //= require react-server 2 | //= require react_ujs 3 | //= require ./components 4 | -------------------------------------------------------------------------------- /test/dummy/app/assets/javascripts/turbolinks_only.js: -------------------------------------------------------------------------------- 1 | //= require turbolinks 2 | -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ApplicationController < ActionController::Base 4 | # Prevent CSRF attacks by raising an exception. 5 | # For APIs, you may want to use :null_session instead. 6 | # protect_from_forgery with: :exception 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/controllers/counters_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CountersController < ApplicationController 4 | def index 5 | @counters = [{ name: "Counter 1" }] 6 | end 7 | 8 | def create 9 | @counter = { name: "Counter 2" } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/pack_components_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PackComponentsController < ApplicationController 4 | # make sure Sprockets application.js isn't loaded: 5 | layout false 6 | def show; end 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/pages_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class PagesController < ApplicationController 4 | per_request_react_rails_prerenderer if ShakapackerHelpers.available? || SprocketsHelpers.available? 5 | 6 | def show 7 | @prerender = !params[:prerender].nil? 8 | if @prerender 9 | js_context = react_rails_prerenderer.context 10 | # This isn't safe for production, we're just testing the render context: 11 | greeting_override = params[:greeting] || "" 12 | setup_code = "global.ctx = {}; global.ctx.greeting = '#{greeting_override}';" 13 | js_context.exec(setup_code) 14 | end 15 | @name = %w[Alice Bob][params[:id].to_i % 2] 16 | render :show 17 | return unless @prerender 18 | 19 | js_context.exec("global.ctx = undefined;") 20 | end 21 | 22 | def no_turbolinks 23 | @prerender = false 24 | render :show, layout: "app_no_turbolinks" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/server_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ServerController < ApplicationController 4 | def show 5 | @component_name = params[:component_name] || "TodoList" 6 | @todos = %w[todo1 todo2 todo3] 7 | end 8 | 9 | def console_example 10 | @todos = %w[todo1 todo2 todo3] 11 | end 12 | 13 | def inline_component_prerender_true 14 | render(component_options) 15 | end 16 | 17 | def inline_component_prerender_false 18 | render(component_options.merge(prerender: false)) 19 | end 20 | 21 | def inline_component_with_camelize_props_prerender_true 22 | render component: "TodoList", props: { test_camelize_props: true, todos: ["dummy"] }, camelize_props: true 23 | end 24 | 25 | def inline_component_with_camelize_props_prerender_false 26 | render component: "TodoList", props: { test_camelize_props: true, todos: ["dummy"] }, camelize_props: true, 27 | prerender: false 28 | end 29 | 30 | private 31 | 32 | def component_options 33 | { 34 | component: "TodoList", 35 | props: { todos: ["Render this inline"] }, 36 | tag: "span", 37 | class: "custom-class", 38 | id: "custom-id", 39 | data: { remote: true } 40 | } 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ApplicationHelper 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/Counter.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var createReactClass = require("create-react-class"); 3 | 4 | module.exports = createReactClass({ 5 | getInitialState: function () { 6 | return { count: 0 }; 7 | }, 8 | handleClick: function () { 9 | this.setState({ count: this.state.count + 1 }); 10 | }, 11 | render: function () { 12 | return ( 13 |
    14 |

    15 | {this.props.name} - {this.state.count} 16 |

    17 | 20 |
    21 | ); 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/GreetingMessage.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | var createReactClass = require("create-react-class") 3 | 4 | module.exports = createReactClass({ 5 | getInitialState: function() { 6 | var initialGreeting = 'Hello'; 7 | if (typeof global !== "undefined" && global.ctx && global.ctx.greeting) { 8 | initialGreeting = global.ctx.greeting 9 | } 10 | 11 | return { 12 | greeting: initialGreeting 13 | } 14 | }, 15 | goodbye: function() { 16 | this.setState({greeting: 'Goodbye'}); 17 | }, 18 | render: function() { 19 | return React.createElement('div', {}, 20 | React.createElement('div', {}, this.state.greeting, ' from Shakapacker ', this.props.name ), 21 | React.createElement('button', {onClick: this.goodbye}, 'Goodbye') 22 | ); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/Todo.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | var createReactClass = require("create-react-class") 3 | 4 | module.exports = createReactClass({ 5 | render: function() { 6 | return React.createElement("li", null, this.props.todo) 7 | } 8 | }) 9 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/TodoList.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | var createReactClass = require("create-react-class") 3 | 4 | module.exports = createReactClass({ 5 | getInitialState: function() { 6 | return({mounted: "nope"}); 7 | }, 8 | componentDidMount: function() { 9 | this.setState({mounted: 'yep'}); 10 | }, 11 | render: function() { 12 | console.log("Test Console Replay") 13 | return ( 14 |
      15 |
    • {this.state.mounted}
    • 16 | {this.props.todos.map(function(todo, i) { 17 | return (
    • {todo}
    • ) 18 | })} 19 |
    • From Shakapacker
    • 20 |
    21 | ) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/TodoListWithConsoleLog.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | var createReactClass = require("create-react-class") 3 | 4 | module.exports = createReactClass({ 5 | getInitialState: function() { 6 | console.log('got initial state'); 7 | return({mounted: "nope"}); 8 | }, 9 | componentWillMount: function() { 10 | // This will need to be replaced 11 | // https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html 12 | console.warn('mounted component'); 13 | this.setState({mounted: 'yep'}); 14 | }, 15 | render: function() { 16 | var x = 'foo'; 17 | console.error('rendered!', x); 18 | return ( 19 |
      20 |
    • Console Logged
    • 21 |
    • {this.state.mounted}
    • 22 | {this.props.todos.map(function(todo, i) { 23 | return (
    • {todo}
    • ) 24 | })} 25 |
    26 | ) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/WithSetTimeout.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | var createReactClass = require("create-react-class") 3 | 4 | module.exports = createReactClass({ 5 | componentWillMount: function () { 6 | setTimeout(function () {}, 1000) 7 | clearTimeout(0) 8 | }, 9 | render: function () { 10 | return I am rendered! 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/export_default_component.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | 3 | export default class ExportDefaultComponent extends React.Component { 4 | render() { 5 | return

    Export Default

    6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/named_export_component.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | 3 | module.exports = { 4 | Component: function(props) { return

    Named Export

    } 5 | } 6 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/components/subfolder/exports_component.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | 3 | module.exports = function(props) { 4 | return

    Exports

    5 | } 6 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/controllers/mount_counters.js: -------------------------------------------------------------------------------- 1 | var { Controller } = require("@hotwired/stimulus"); 2 | var ReactRailsUJS = require("react_ujs"); 3 | 4 | module.exports = class extends Controller { 5 | connect() { 6 | this.observeChange(); 7 | } 8 | 9 | disconnect() { 10 | this.observer.disconnect(); 11 | } 12 | 13 | observeChange() { 14 | var element = this.element; 15 | var callback = function (mutationsList, _observer) { 16 | mutationsList.forEach(function (mutation) { 17 | if (mutation.type === "childList") { 18 | ReactRailsUJS.mountComponents(element); 19 | } 20 | }); 21 | }; 22 | 23 | this.observer = new MutationObserver(callback); 24 | this.observer.observe(this.element, { childList: true }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/application.js: -------------------------------------------------------------------------------- 1 | require("@hotwired/turbo-rails"); 2 | var { Application } = require("@hotwired/stimulus"); 3 | var MountCountersController = require("../controllers/mount_counters"); 4 | 5 | window.Stimulus = Application.start(); 6 | Stimulus.register("mount-counters", MountCountersController); 7 | 8 | var ctx = require.context("components", true); 9 | var ReactRailsUJS = require("react_ujs"); 10 | ReactRailsUJS.useContext(ctx); 11 | var React = require("react"); 12 | 13 | window.GlobalComponent = function (props) { 14 | return React.createElement("h1", null, "Global Component"); 15 | }; 16 | -------------------------------------------------------------------------------- /test/dummy/app/javascript/packs/server_rendering.js: -------------------------------------------------------------------------------- 1 | // By default, this pack is loaded for server-side rendering. 2 | // It must expose react_ujs as `ReactRailsUJS` and prepare a require context. 3 | var componentRequireContext = require.context("components", true); 4 | var ReactRailsUJS = require("react_ujs"); 5 | ReactRailsUJS.useContext(componentRequireContext); 6 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/app/mailers/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/app/models/.keep -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/pants/yfronts.js: -------------------------------------------------------------------------------- 1 | // used for testing file watcher configuration 2 | -------------------------------------------------------------------------------- /test/dummy/app/views/counters/create.turbo_stream.erb: -------------------------------------------------------------------------------- 1 | <%= turbo_stream.append :counters do %> 2 | <%= react_component("Counter", @counter) %> 3 | <% end %> 4 | -------------------------------------------------------------------------------- /test/dummy/app/views/counters/index.html.erb: -------------------------------------------------------------------------------- 1 |

    React 18 bug reproduction

    2 | 3 | <%= turbo_frame_tag :counters, data: { controller: "mount-counters" } do %> 4 | <% @counters.each do |counter| %> 5 | <%= react_component("Counter", counter) %> 6 | <% end %> 7 | <% end %> 8 | <%= form_with(url: counters_path, method: :post, data: { turbo: true, turbo_stream: true }) do |form| %> 9 | <%= form.submit "Add counter" %> 10 | <% end %> 11 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/app_no_turbolinks.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= javascript_include_tag "app_no_turbolinks" %> 6 | <%= csrf_meta_tags %> 7 | 8 | 9 | 10 | <%= yield %> 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <% if ShakapackerHelpers.available? %> 6 | <%= javascript_pack_tag "application" %> 7 | <% elsif SprocketsHelpers.available? %> 8 | <%= javascript_include_tag "application", "data-turbolinks-track" => true %> 9 | <% end %> 10 | <%= csrf_meta_tags %> 11 | 12 | 13 | 14 | <%= yield %> 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/dummy/app/views/pack_components/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= javascript_pack_tag "application" if ShakapackerHelpers.available? %> 2 | 3 | 4 | <%= react_component("export_default_component") %> 5 | <%= react_component("named_export_component.Component") %> 6 | <%= react_component("subfolder/exports_component") %> 7 | <%= react_component("GlobalComponent") %> 8 | -------------------------------------------------------------------------------- /test/dummy/app/views/pages/_component_with_inner_html.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component 'GreetingMessage', { :name => 'Name' }, { :id => 'component' } do %> 2 |
    NestedContent
    3 | <% end %> 4 | -------------------------------------------------------------------------------- /test/dummy/app/views/pages/show.html.erb: -------------------------------------------------------------------------------- 1 |
      2 |
    • <%= link_to 'Alice', page_path(id: 0) %>
    • 3 |
    • <%= link_to 'Bob', page_path(id: 1) %>
    • 4 |
    5 | 6 |
    7 | <%= react_component 'GreetingMessage', { name: @name, lastName: "Last #{@name}", info: { name: @name, lastName: "Last #{@name}" } }, { id: 'component', class: "greeting-message", prerender: @prerender } %> 8 |
      9 | <%= react_component 'Todo', { todo: 'Another Component' }, { id: 'todo', prerender: @prerender } %> 10 |
    11 |
    12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/dummy/app/views/server/console_example.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component "TodoListWithConsoleLog", {todos: @todos}, {prerender: true} %> 2 | -------------------------------------------------------------------------------- /test/dummy/app/views/server/console_example_suppressed.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component "TodoListWithConsoleLog", {todos: @todos}, {prerender: true} %> 2 | -------------------------------------------------------------------------------- /test/dummy/app/views/server/show.html.erb: -------------------------------------------------------------------------------- 1 | <%= react_component @component_name, {todos: @todos}, {prerender: true} %> 2 | -------------------------------------------------------------------------------- /test/dummy/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | const defaultConfigFunc = require('shakapacker/package/babel/preset.js') 3 | const resultConfig = defaultConfigFunc(api) 4 | const isProductionEnv = api.env('production') 5 | const isDevelopmentEnv = api.env('development') 6 | 7 | const changesOnDefault = { 8 | presets: [ 9 | [ 10 | '@babel/preset-react', 11 | { 12 | development: !isProductionEnv, 13 | useBuiltIns: true 14 | } 15 | ] 16 | ].filter(Boolean), 17 | plugins: [ 18 | process.env.WEBPACK_SERVE && 'react-refresh/babel', 19 | !isDevelopmentEnv && [ 20 | 'babel-plugin-transform-react-remove-prop-types', 21 | { 22 | removeImport: true, 23 | }, 24 | ], 25 | ].filter(Boolean), 26 | } 27 | 28 | resultConfig.presets = [...resultConfig.presets, ...changesOnDefault.presets] 29 | resultConfig.plugins = [...resultConfig.plugins, ...changesOnDefault.plugins ] 30 | 31 | return resultConfig 32 | } 33 | -------------------------------------------------------------------------------- /test/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 5 | load Gem.bin_path("bundler", "bundle") 6 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | APP_PATH = File.expand_path("../config/application", __dir__) 5 | require_relative "../config/boot" 6 | require "rails/commands" 7 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require_relative "../config/boot" 5 | require "rake" 6 | Rake.application.run 7 | -------------------------------------------------------------------------------- /test/dummy/bin/shakapacker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "pathname" 5 | require "bundler/setup" 6 | require "shakapacker" 7 | require "shakapacker/webpack_runner" 8 | 9 | ENV["RAILS_ENV"] ||= "development" 10 | ENV["NODE_ENV"] ||= ENV.fetch("RAILS_ENV", nil) 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath) 12 | 13 | APP_ROOT = File.expand_path("..", __dir__) 14 | Dir.chdir(APP_ROOT) do 15 | Shakapacker::WebpackRunner.run(ARGV) 16 | end 17 | -------------------------------------------------------------------------------- /test/dummy/bin/shakapacker-dev-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | ENV["RAILS_ENV"] ||= "development" 5 | ENV["NODE_ENV"] ||= ENV.fetch("RAILS_ENV", nil) 6 | 7 | require "pathname" 8 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 9 | Pathname.new(__FILE__).realpath) 10 | 11 | require "bundler/setup" 12 | 13 | require "shakapacker" 14 | require "shakapacker/dev_server_runner" 15 | 16 | APP_ROOT = File.expand_path("..", __dir__) 17 | Dir.chdir(APP_ROOT) do 18 | Shakapacker::DevServerRunner.run(ARGV) 19 | end 20 | -------------------------------------------------------------------------------- /test/dummy/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | VENDOR_PATH = File.expand_path("..", __dir__) 5 | Dir.chdir(VENDOR_PATH) do 6 | exec "yarnpkg #{ARGV.join(' ')}" 7 | rescue Errno::ENOENT 8 | puts "Yarn executable was not detected in the system." 9 | puts "Download Yarn at https://yarnpkg.com/en/docs/install" 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This file is used by Rack-based servers to start the application. 4 | 5 | require File.expand_path("config/environment", __dir__) 6 | run Rails.application 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.expand_path("boot", __dir__) 4 | require_relative("../../support/sprockets_helpers") 5 | require_relative("../../support/webpacker_helpers") 6 | 7 | # Pick the frameworks you want: 8 | # require "active_record/railtie" 9 | require "action_controller/railtie" 10 | 11 | # Test no-sprockets environment by testing the gemfile name 12 | require "sprockets/railtie" if SprocketsHelpers.available? 13 | 14 | require "rails/test_unit/railtie" 15 | 16 | # Make sure gems in development group are required, for example, react-rails and turbolinks. 17 | # These gems are specified in .gemspec file by add_development_dependency. They are not runtime 18 | # dependencies for react-rails project but probably runtime dependencies for this dummy rails app. 19 | Bundler.require(*(Rails.groups | ["development"])) 20 | 21 | module Dummy 22 | class Application < Rails::Application 23 | # Initialize configuration defaults for originally generated Rails version. 24 | config.load_defaults 7.0 25 | 26 | # Settings in config/environments/* take precedence over those specified here. 27 | # Application configuration should go into files in config/initializers 28 | # -- all .rb files in that directory are automatically loaded. 29 | 30 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 31 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 32 | # config.time_zone = 'Central Time (US & Canada)' 33 | 34 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 35 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 36 | # config.i18n.default_locale = :de 37 | config.react.variant = :production 38 | config.react.server_renderer_options = { 39 | replay_console: true 40 | } 41 | 42 | if SprocketsHelpers.available? 43 | config.assets.precompile += %w[app_no_turbolinks.js] 44 | config.assets.enabled = true 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Set up gems listed in the Gemfile. 4 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) 5 | 6 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 7 | $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) 8 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Load the Rails application. 4 | require File.expand_path("application", __dir__) 5 | 6 | # Initialize the Rails application. 7 | Dummy::Application.initialize! 8 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # In the development environment your application's code is reloaded on 7 | # every request. This slows down response time but is perfect for development 8 | # since you don't have to restart the web server when you make code changes. 9 | config.cache_classes = false 10 | 11 | # Do not eager load code on boot. 12 | config.eager_load = false 13 | 14 | # Show full error reports and disable caching. 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Print deprecation notices to the Rails logger. 19 | config.active_support.deprecation = :log 20 | end 21 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Code is not reloaded between requests. 7 | config.cache_classes = true 8 | 9 | # Eager load code on boot. This eager loads most of Rails and 10 | # your application in memory, allowing both thread web servers 11 | # and those relying on copy on write to perform better. 12 | # Rake tasks automatically ignore this option for performance. 13 | config.eager_load = true 14 | 15 | # Full error reports are disabled and caching is turned on. 16 | config.consider_all_requests_local = false 17 | config.action_controller.perform_caching = true 18 | 19 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 20 | # Add `rack-cache` to your Gemfile before enabling this. 21 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 22 | # config.action_dispatch.rack_cache = true 23 | 24 | # Disable Rails's static asset server (Apache or nginx will already do this). 25 | config.serve_static_assets = false 26 | 27 | # Compress JavaScripts and CSS. 28 | config.assets.js_compressor = :uglifier 29 | # config.assets.css_compressor = :sass 30 | 31 | # Do not fallback to assets pipeline if a precompiled asset is missed. 32 | config.assets.compile = false 33 | 34 | # Generate digests for assets URLs. 35 | config.assets.digest = true 36 | 37 | # Version of your assets, change this if you want to expire all your assets. 38 | config.assets.version = "1.0" 39 | 40 | # Specifies the header that your server uses for sending files. 41 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 43 | 44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 45 | # config.force_ssl = true 46 | 47 | # Set to :debug to see everything in the log. 48 | config.log_level = :info 49 | 50 | # Prepend all log lines with the following tags. 51 | # config.log_tags = [ :subdomain, :uuid ] 52 | 53 | # Use a different logger for distributed setups. 54 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 55 | 56 | # Use a different cache store in production. 57 | # config.cache_store = :mem_cache_store 58 | 59 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 60 | # config.action_controller.asset_host = "http://assets.example.com" 61 | 62 | # Precompile additional assets. 63 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 64 | # config.assets.precompile += %w( search.js ) 65 | 66 | # Ignore bad email addresses and do not raise email delivery errors. 67 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 68 | # config.action_mailer.raise_delivery_errors = false 69 | 70 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 71 | # the I18n.default_locale when a translation can not be found). 72 | config.i18n.fallbacks = true 73 | 74 | # Send deprecation notices to registered listeners. 75 | config.active_support.deprecation = :notify 76 | 77 | # Disable automatic flushing of the log to improve performance. 78 | # config.autoflush_log = false 79 | 80 | # Use default logging formatter so that PID and timestamp are not suppressed. 81 | config.log_formatter = Logger::Formatter.new 82 | end 83 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # The test environment is used exclusively to run your application's 7 | # test suite. You never need to work with it otherwise. Remember that 8 | # your test database is "scratch space" for the test suite and is wiped 9 | # and recreated between test runs. Don't rely on the data there! 10 | 11 | # we need this to reload the jsx transformer when different version is dropped in 12 | config.cache_classes = false 13 | config.reload_plugins = true 14 | config.assets.cache_store = :null_store if SprocketsHelpers.available? 15 | 16 | # Do not eager load code on boot. This avoids loading your whole application 17 | # just for the purpose of running a single test. If you are using a tool that 18 | # preloads Rails for running tests, you may have to set it to true. 19 | config.eager_load = false 20 | 21 | # Configure static asset server for tests with Cache-Control for performance. 22 | # Disabled since we dont use it and this option is deprecated from Rails 4.2 onwards 23 | # config.serve_static_assets = true 24 | config.static_cache_control = "public, max-age=3600" 25 | 26 | # Show full error reports and disable caching. 27 | config.consider_all_requests_local = true 28 | config.action_controller.perform_caching = false 29 | 30 | # Raise exceptions instead of rendering exception templates. 31 | config.action_dispatch.show_exceptions = false 32 | 33 | # Disable request forgery protection in test environment. 34 | config.action_controller.allow_forgery_protection = false 35 | 36 | # Print deprecation notices to the stderr. 37 | config.active_support.deprecation = :stderr 38 | 39 | config.react.variant = :test 40 | end 41 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 6 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 7 | 8 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 9 | # Rails.backtrace_cleaner.remove_silencers! 10 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Configure sensitive parameters which will be filtered from the log file. 6 | Rails.application.config.filter_parameters += [:password] 7 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new inflection rules using the following format. Inflections 6 | # are locale specific, and you may define rules for as many different 7 | # locales as you wish. All of these examples are active by default: 8 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 9 | # inflect.plural /^(ox)$/i, '\1en' 10 | # inflect.singular /^(ox)en/i, '\1' 11 | # inflect.irregular 'person', 'people' 12 | # inflect.uncountable %w( fish sheep ) 13 | # end 14 | 15 | # These inflection rules are supported but not enabled by default: 16 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 17 | # inflect.acronym 'RESTful' 18 | # end 19 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Add new mime types for use in respond_to blocks: 6 | # Mime::Type.register "text/richtext", :rtf 7 | # Mime::Type.register_alias "text/html", :iphone 8 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/react.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Override setting set in application.rb 4 | class CustomComponentMount < React::Rails::ComponentMount 5 | end 6 | 7 | Dummy::Application.configure do 8 | config.react.view_helper_implementation = CustomComponentMount 9 | # Add "app/pants" to the array we can test that file watchers are setup after 10 | # rails initializers are loaded 11 | config.react.server_renderer_directories = ["/app/assets/javascripts/", 12 | "app/javascript", 13 | "app/pants"] 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # Your secret key is used for verifying the integrity of signed cookies. 6 | # If you change this key, all old signed cookies will become invalid! 7 | 8 | # Make sure the secret is at least 30 characters and all random, 9 | # no regular words or you'll be exposed to dictionary attacks. 10 | # You can use `rake secret` to generate a secure secret key. 11 | 12 | # Make sure your secret_key_base is kept private 13 | # if you're sharing your code publicly. 14 | Dummy::Application.config.secret_key_base = "43fa5672451bbd0a171668e625edc433eb00eeeb14c2606546e262e499ab853cfb532998d4809abe5019bf13888863e3a2c7d5cf7757de7a2b1fb50826d9874e" # rubocop:disable Layout/LineLength 15 | 16 | # For Rails 3.2. 17 | Dummy::Application.config.secret_token = Dummy::Application.config.secret_key_base 18 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | Dummy::Application.config.session_store :cookie_store, key: "_dummy_session" 6 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Be sure to restart your server when you modify this file. 4 | 5 | # This file contains settings for ActionController::ParamsWrapper which 6 | # is enabled by default. 7 | 8 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 9 | ActiveSupport.on_load(:action_controller) do 10 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 11 | end 12 | 13 | # To enable root element in JSON for ActiveRecord objects. 14 | # ActiveSupport.on_load(:active_record) do 15 | # self.include_root_in_json = true 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Dummy::Application.routes.draw do 4 | get "no-turbolinks", to: "pages#no_turbolinks" 5 | resources :pages, only: [:show] 6 | resources :counters, only: %i[create index] 7 | resources :server, only: [:show] do 8 | collection do 9 | get :console_example 10 | get :inline_component_prerender_true 11 | get :inline_component_prerender_false 12 | get :inline_component_with_camelize_props_prerender_true 13 | get :inline_component_with_camelize_props_prerender_false 14 | end 15 | end 16 | 17 | resource :pack_component, only: :show 18 | end 19 | -------------------------------------------------------------------------------- /test/dummy/config/shakapacker.yml: -------------------------------------------------------------------------------- 1 | # Note: You must restart bin/webpack-dev-server for changes to take effect 2 | 3 | default: &default 4 | source_path: app/javascript 5 | source_entry_path: packs 6 | public_output_path: packs 7 | cache_path: tmp/cache/shakapacker 8 | 9 | # Additional paths webpack should lookup modules 10 | # ['app/assets', 'engine/foo/app/assets'] 11 | resolved_paths: [] 12 | 13 | # Reload manifest.json on all requests so we reload latest compiled packs 14 | cache_manifest: false 15 | 16 | development: 17 | <<: *default 18 | compile: true 19 | 20 | dev_server: 21 | host: localhost 22 | port: 8080 23 | hmr: false 24 | https: false 25 | 26 | test: 27 | <<: *default 28 | compile: true 29 | 30 | dev_server: 31 | host: localhost 32 | port: 8080 33 | hmr: false 34 | https: false 35 | 36 | production: 37 | <<: *default 38 | 39 | # Production depends on precompilation of packs prior to booting for performance. 40 | compile: false 41 | 42 | # Cache manifest.json for performance 43 | cache_manifest: true 44 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/clientWebpackConfig.js: -------------------------------------------------------------------------------- 1 | const commonWebpackConfig = require('./commonWebpackConfig') 2 | 3 | const configureClient = () => { 4 | const clientConfig = commonWebpackConfig() 5 | 6 | // server-bundle is special and should ONLY be built by the serverConfig 7 | // In case this entry is not deleted, a very strange "window" not found 8 | // error shows referring to window["webpackJsonp"]. That is because the 9 | // client config is going to try to load chunks. 10 | delete clientConfig.entry['server_rendering'] 11 | 12 | return clientConfig 13 | } 14 | 15 | module.exports = configureClient 16 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/commonWebpackConfig.js: -------------------------------------------------------------------------------- 1 | // Common configuration applying to client and server configuration 2 | 3 | const { generateWebpackConfig, merge } = require('shakapacker') 4 | 5 | const baseClientWebpackConfig = generateWebpackConfig() 6 | 7 | const commonOptions = { 8 | resolve: { 9 | extensions: ['.css', '.ts', '.tsx'] 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.mdx?$/, 15 | use: [ 16 | { 17 | loader: '@mdx-js/loader', 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | // Uncommemt if getting "Module not found: Error: Can't resolve 'react-dom/client'" warning 24 | // ignoreWarnings: [/Module not found: Error: Can't resolve 'react-dom\/client'/] 25 | } 26 | 27 | // Copy the object using merge b/c the baseClientWebpackConfig and commonOptions are mutable globals 28 | const commonWebpackConfig = () => (merge({}, baseClientWebpackConfig, commonOptions)) 29 | 30 | module.exports = commonWebpackConfig 31 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/development.js: -------------------------------------------------------------------------------- 1 | const { devServer, inliningCss } = require('shakapacker') 2 | 3 | const webpackConfig = require('./serverClientOrBoth') 4 | 5 | const developmentEnvOnly = (clientWebpackConfig, serverWebpackConfig) => { 6 | 7 | //plugins 8 | if (inliningCss ) { 9 | // Note, when this is run, we're building the server and client bundles in separate processes. 10 | // Thus, this plugin is not applied. 11 | const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin') 12 | clientWebpackConfig.plugins.push( 13 | new ReactRefreshWebpackPlugin({ 14 | overlay:{ 15 | sockPort: devServer.port 16 | } 17 | }) 18 | ) 19 | } 20 | } 21 | module.exports = webpackConfig(developmentEnvOnly) 22 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/production.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./serverClientOrBoth') 2 | 3 | const productionEnvOnly = (_clientWebpackConfig, _serverWebpackConfig) => { 4 | // place any code here that is for production only 5 | } 6 | 7 | module.exports = webpackConfig(productionEnvOnly) 8 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/serverClientOrBoth.js: -------------------------------------------------------------------------------- 1 | const clientWebpackConfig = require('./clientWebpackConfig') 2 | const serverWebpackConfig = require('./serverWebpackConfig') 3 | 4 | const webpackConfig = (envSpecific) => { 5 | const clientConfig = clientWebpackConfig() 6 | const serverConfig = serverWebpackConfig() 7 | 8 | if (envSpecific) { 9 | envSpecific(clientConfig, serverConfig) 10 | } 11 | 12 | let result 13 | // For HMR, need to separate the the client and server webpack configurations 14 | if (process.env.WEBPACK_SERVE || process.env.CLIENT_BUNDLE_ONLY) { 15 | // eslint-disable-next-line no-console 16 | console.log('[React on Rails] Creating only the client bundles.') 17 | result = clientConfig 18 | } else if (process.env.SERVER_BUNDLE_ONLY) { 19 | // eslint-disable-next-line no-console 20 | console.log('[React on Rails] Creating only the server bundle.') 21 | result = serverConfig 22 | } else { 23 | // default is the standard client and server build 24 | // eslint-disable-next-line no-console 25 | console.log('[React on Rails] Creating both client and server bundles.') 26 | result = [clientConfig, serverConfig] 27 | } 28 | 29 | // To debug, uncomment next line and inspect "result" 30 | // debugger 31 | return result 32 | } 33 | 34 | module.exports = webpackConfig 35 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/test.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./serverClientOrBoth') 2 | 3 | const testOnly = (_clientWebpackConfig, _serverWebpackConfig) => { 4 | // place any code here that is for test only 5 | } 6 | 7 | module.exports = webpackConfig(testOnly) 8 | -------------------------------------------------------------------------------- /test/dummy/config/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require('fs'); 2 | const { resolve } = require('path'); 3 | const { env, generateWebpackConfig } = require('shakapacker'); 4 | 5 | const envSpecificConfig = () => { 6 | const path = resolve(__dirname, `${env.nodeEnv}.js`); 7 | if (existsSync(path)) { 8 | console.log(`Loading ENV specific webpack configuration file ${path}`); 9 | return require(path); 10 | } else { 11 | return generateWebpackConfig(); 12 | } 13 | }; 14 | 15 | module.exports = envSpecificConfig(); 16 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "yalc-postinstall": "yalc link react_ujs" 4 | }, 5 | "dependencies": { 6 | "@babel/core": "^7.18.2", 7 | "@babel/plugin-transform-runtime": "^7.18.2", 8 | "@babel/preset-env": "^7.18.2", 9 | "@babel/preset-react": "^7.17.12", 10 | "@babel/preset-typescript": "^7.17.12", 11 | "@babel/runtime": "^7.18.3", 12 | "@hotwired/stimulus": "^3.2.2", 13 | "@hotwired/turbo-rails": "^7.3.0", 14 | "babel-loader": "^8.2.5", 15 | "babel-plugin-macros": "^3.1.0", 16 | "compression-webpack-plugin": "^9.2.0", 17 | "create-react-class": "^15.6.2", 18 | "css-loader": "^5.2.7", 19 | "css-minimizer-webpack-plugin": "^2.0.0", 20 | "mini-css-extract-plugin": "^1.6.2", 21 | "pnp-webpack-plugin": "^1.7.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react_ujs": "file:.yalc/react_ujs", 25 | "shakapacker": "7.2.0", 26 | "style-loader": "^3.3.1", 27 | "terser-webpack-plugin": "^5.3.3", 28 | "webpack": "^5.73.0", 29 | "webpack-assets-manifest": "^5.1.0", 30 | "webpack-cli": "^4.9.2", 31 | "webpack-merge": "^5.8.0", 32 | "webpack-sources": "^3.2.3" 33 | }, 34 | "devDependencies": { 35 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", 36 | "babel-plugin-transform-react-remove-prop-types": "^0.4.24", 37 | "react-refresh": "^0.13.0", 38 | "webpack-dev-server": "^4.9.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The page you were looking for doesn't exist.

    54 |

    You may have mistyped the address or the page may have moved.

    55 |
    56 |

    If you are the application owner check the logs for more information.

    57 | 58 | 59 | -------------------------------------------------------------------------------- /test/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    The change you wanted was rejected.

    54 |

    Maybe you tried to change something you didn't have access to.

    55 |
    56 |

    If you are the application owner check the logs for more information.

    57 | 58 | 59 | -------------------------------------------------------------------------------- /test/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 48 | 49 | 50 | 51 | 52 |
    53 |

    We're sorry, but something went wrong.

    54 |
    55 |

    If you are the application owner check the logs for more information.

    56 | 57 | 58 | -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/vendor/assets/javascripts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactjs/react-rails/6e811b6bc3634112a269821ee2f5b512167d39ae/test/dummy/vendor/assets/javascripts/.gitkeep -------------------------------------------------------------------------------- /test/dummy/vendor/assets/react/JSXTransformer__.js: -------------------------------------------------------------------------------- 1 | var JSXTransformer = { 2 | transform: function () { 3 | return { 4 | code: 'test_confirmation_token_jsx_transformed;' 5 | }; 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/dummy/vendor/assets/react/test/react__.js: -------------------------------------------------------------------------------- 1 | 'test_confirmation_token_react_content'; -------------------------------------------------------------------------------- /test/generators/coffee_component_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "generators/react/component_generator" 5 | 6 | class CoffeeComponentGeneratorTest < Rails::Generators::TestCase 7 | destination File.join(Rails.root, "tmp", "component_generator_test_output") 8 | setup :prepare_destination 9 | tests React::Generators::ComponentGenerator 10 | 11 | if ShakapackerHelpers.available? 12 | def filename 13 | "app/javascript/components/GeneratedComponent.coffee" 14 | end 15 | 16 | test "that Shakapacker defaults to ES6" do 17 | run_generator %w[GeneratedComponent name] 18 | 19 | es6 = File.read(File.join(destination_root, "app/javascript/components/GeneratedComponent.js")) 20 | 21 | assert_match(/const GeneratedComponent = \(props\) => {/, es6) 22 | end 23 | else 24 | def filename 25 | "app/assets/javascripts/components/generated_component.js.jsx.coffee" 26 | end 27 | end 28 | def class_name 29 | "GeneratedComponent" 30 | end 31 | 32 | test "that it the uses CoffeeScript syntax" do 33 | run_generator %w[GeneratedComponent name --coffee] 34 | 35 | assert_file filename, /^class #{class_name}\sextends\sReact\.Component/ 36 | end 37 | 38 | test "that propTypes get assigned" do 39 | run_generator %w[GeneratedComponent name --coffee] 40 | 41 | assert_file filename, /@propTypes\s=/ 42 | assert_file filename, /PropTypes/ 43 | end 44 | 45 | test "that it generates working jsx" do 46 | run_generator %w[GeneratedComponent name:string address:shape --coffee] 47 | jsx = React::JSX.transform(CoffeeScript.compile(File.read(File.join(destination_root, filename)))) 48 | 49 | assert_match(Regexp.new(expected_working_jsx), jsx) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/generators/component_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "generators/react/component_generator" 5 | 6 | class ComponentGeneratorTest < Rails::Generators::TestCase 7 | destination File.join(Rails.root, "tmp", "component_generator_test_output") 8 | setup :prepare_destination 9 | tests React::Generators::ComponentGenerator 10 | 11 | if ShakapackerHelpers.available? 12 | def filename 13 | "app/javascript/components/GeneratedComponent.js" 14 | end 15 | 16 | def filename_with_subfolder 17 | "app/javascript/components/generated_folder/GeneratedComponent.js" 18 | end 19 | else 20 | def filename 21 | "app/assets/javascripts/components/generated_component.js.jsx" 22 | end 23 | 24 | def filename_with_subfolder 25 | "app/assets/javascripts/components/generated_folder/generated_component.js.jsx" 26 | end 27 | end 28 | 29 | test "creates the component file" do 30 | run_generator %w[GeneratedComponent] 31 | 32 | assert_file filename do |contents| 33 | if ShakapackerHelpers.available? 34 | assert_match(/^import React from "react"/, contents) 35 | assert_match(/export default GeneratedComponent\n$/m, contents) 36 | end 37 | end 38 | end 39 | 40 | test "creates the component file in a subdirectory" do 41 | run_generator %w[generated_folder/GeneratedComponent] 42 | assert_file filename_with_subfolder do |contents| 43 | if ShakapackerHelpers.available? 44 | assert_match(/^import React from "react"/, contents) 45 | assert_match(/export default GeneratedComponent\n$/m, contents) 46 | end 47 | end 48 | end 49 | 50 | test "creates the component file with a node argument" do 51 | run_generator %w[GeneratedComponent name] 52 | 53 | assert_file filename, /name: PropTypes.node/ 54 | end 55 | 56 | test "creates the component file with various standard proptypes" do 57 | proptypes = %w[string bool number array func number object any] 58 | run_generator %w[GeneratedComponent] + proptypes.map { |type| "my_#{type}:#{type}" } 59 | 60 | proptypes.each do |type| 61 | assert_file filename, /my#{type.capitalize}: PropTypes.#{type}/ 62 | end 63 | end 64 | 65 | test "creates a component file with an instanceOf property" do 66 | run_generator %w[GeneratedComponent favorite_food:instanceOf{food}] 67 | 68 | assert_file filename, /favoriteFood: PropTypes.instanceOf\(Food\)/ 69 | end 70 | 71 | test "creates a component file with a oneOf property" do 72 | run_generator %w[GeneratedComponent favorite_food:oneOf{pizza,hamburgers}] 73 | 74 | assert_file filename, /favoriteFood: PropTypes.oneOf\(\['pizza','hamburgers'\]\)/ 75 | end 76 | 77 | test "creates a component file with a oneOfType property" do 78 | run_generator %w[GeneratedComponent favorite_food:oneOfType{string,Food}] 79 | expected_property = "favoriteFood: PropTypes.oneOfType([PropTypes.string,PropTypes.instanceOf(Food)])" 80 | 81 | assert_file filename, Regexp.new(Regexp.quote(expected_property)) 82 | end 83 | 84 | test "generates working jsx" do 85 | run_generator %w[GeneratedComponent name:string address:shape] 86 | jsx = React::JSX.transform(File.read(File.join(destination_root, filename))) 87 | 88 | assert_match(Regexp.new(expected_working_jsx_in_function_component), jsx) 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/generators/es6_component_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "generators/react/component_generator" 5 | 6 | class Es6ComponentGeneratorTest < Rails::Generators::TestCase 7 | destination File.join(Rails.root, "tmp", "component_generator_test_output") 8 | setup :prepare_destination 9 | tests React::Generators::ComponentGenerator 10 | 11 | if ShakapackerHelpers.available? 12 | def filename 13 | "app/javascript/components/GeneratedComponent.js" 14 | end 15 | else 16 | def filename 17 | "app/assets/javascripts/components/generated_component.es6.jsx" 18 | end 19 | end 20 | 21 | def component_name 22 | "GeneratedComponent" 23 | end 24 | 25 | test "uses es6 syntax" do 26 | run_generator %w[GeneratedComponent name --es6] 27 | 28 | assert_file filename, /const #{component_name} = \(props\) => {/ 29 | end 30 | 31 | test "assigns defaultProps after function definintion" do 32 | run_generator %w[GeneratedComponent name --es6] 33 | 34 | assert_file filename, /\s^#{component_name}\.propTypes/ 35 | end 36 | 37 | test "generates working jsx" do 38 | run_generator %w[GeneratedComponent name:string address:shape --es6] 39 | jsx = React::JSX.transform(File.read(File.join(destination_root, filename))) 40 | 41 | assert_match(Regexp.new(expected_working_jsx_in_function_component), jsx) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/generators/install_generator_sprockets_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "generators/react/install_generator" 5 | 6 | # If Shakapacker is available, its setup is preferred 7 | unless ShakapackerHelpers.available? 8 | class InstallGeneratorSprocketsTest < Rails::Generators::TestCase 9 | destination File.join(Rails.root, "tmp", "generator_test_output") 10 | tests React::Generators::InstallGenerator 11 | setup :prepare_destination 12 | 13 | def copy_directory(dir) 14 | source = Rails.root.join(dir) 15 | dest = Rails.root.join(destination_root, File.dirname(dir)) 16 | 17 | FileUtils.mkdir_p dest 18 | FileUtils.cp_r source, dest 19 | end 20 | 21 | test "adds requires to `application.js`" do 22 | run_generator 23 | 24 | assert_application_file_created 25 | end 26 | 27 | test "it modifes an existing 'application.js'" do 28 | copy_directory("app/assets/javascripts/application.js") 29 | run_generator 30 | 31 | assert_application_file_modified 32 | end 33 | 34 | test "creates `application.js` if it doesn't exist" do 35 | copy_directory("app/assets/javascripts/application.js") 36 | File.delete "#{destination_root}/app/assets/javascripts/application.js" 37 | 38 | run_generator 39 | 40 | assert_application_file_created 41 | end 42 | 43 | test "modifies `application.js` if it's empty" do 44 | init_application_js "" 45 | 46 | run_generator 47 | 48 | assert_application_file_created 49 | end 50 | 51 | test "updates `application.js` if require_tree is commented" do 52 | init_application_js <<-DIRECTIVE 53 | // 54 | // require_tree . 55 | // 56 | DIRECTIVE 57 | 58 | run_generator 59 | 60 | assert_application_file_modified 61 | end 62 | 63 | test "updates `application.js` if require turbolinks has extra spaces" do 64 | init_application_js <<-DIRECTIVE 65 | // 66 | //= require turbolinks#{' '} 67 | // 68 | DIRECTIVE 69 | 70 | run_generator 71 | 72 | assert_application_file_modified 73 | end 74 | 75 | test "creates server_rendering.js with default requires" do 76 | run_generator 77 | server_rendering_file_path = "app/assets/javascripts/server_rendering.js" 78 | 79 | assert_file server_rendering_file_path, %r{//= require react-server\n} 80 | assert_file server_rendering_file_path, %r{//= require ./components\n} 81 | end 82 | 83 | test "creates server rendering initializer" do 84 | run_generator 85 | initializer_path = "config/initializers/react_server_rendering.rb" 86 | 87 | assert_file(initializer_path, /Rails.application.config.assets.precompile \+= \["server_rendering.js"\]/) 88 | end 89 | 90 | test "skipping server rendering" do 91 | run_generator %w[--skip-server-rendering] 92 | 93 | assert_no_file "config/initializers/react_server_rendering.rb" 94 | assert_no_file "app/assets/javascripts/server_rendering.js" 95 | end 96 | 97 | def init_application_js(content) 98 | FileUtils.mkdir_p "#{destination_root}/app/assets/javascripts/" 99 | File.write "#{destination_root}/app/assets/javascripts/application.js", content 100 | end 101 | 102 | private 103 | 104 | def assert_application_file_created 105 | assert_file "app/assets/javascripts/application.js", 106 | %r{//= require react\n//= require react_ujs\n//= require components\n} 107 | end 108 | 109 | def assert_application_file_modified 110 | assert_file "app/assets/javascripts/application.js", %r{\n//= require react\n} 111 | assert_file "app/assets/javascripts/application.js", %r{\n//= require react_ujs\n} 112 | assert_file "app/assets/javascripts/application.js", %r{\n//= require components\n} 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/generators/install_generator_webpacker_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "generators/react/install_generator" 5 | class InstallGeneratorShakapackerTest < Rails::Generators::TestCase 6 | ShakapackerHelpers.when_shakapacker_available do 7 | destination File.join(Rails.root, "tmp", "generator_test_output") 8 | tests React::Generators::InstallGenerator 9 | setup :prepare_destination 10 | 11 | expected_setup = %|// Support component names relative to this directory: 12 | var componentRequireContext = require.context("components", true); 13 | var ReactRailsUJS = require("react_ujs"); 14 | ReactRailsUJS.useContext(componentRequireContext); 15 | | 16 | 17 | default_server_rendering_pack_path = "app/javascript/packs/server_rendering.js" 18 | 19 | def copy_directory(dir) 20 | source = Rails.root.join(dir) 21 | dest = Rails.root.join(destination_root, File.dirname(dir)) 22 | 23 | FileUtils.mkdir_p dest 24 | FileUtils.cp_r source, dest 25 | end 26 | 27 | test "adds requires to `application.js`" do 28 | run_generator 29 | 30 | assert_file "app/javascript/packs/application.js", expected_setup 31 | assert_file "app/javascript/components" 32 | end 33 | 34 | test "creates server_rendering.js with default requires" do # rubocop:disable Minitest/MultipleAssertions 35 | run_generator 36 | assert_file default_server_rendering_pack_path do |contents| 37 | assert_includes contents, "var componentRequireContext = require.context(\"components\", true);\n" 38 | assert_includes contents, "var ReactRailsUJS = require(\"react_ujs\");\n" 39 | assert_includes contents, "ReactRailsUJS.useContext(componentRequireContext);\n" 40 | end 41 | end 42 | 43 | test "skipping server rendering" do 44 | run_generator %w[--skip-server-rendering] 45 | 46 | assert_no_file default_server_rendering_pack_path 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/generators/ts_es6_component_generator_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "generators/react/component_generator" 5 | 6 | class TsEs6ComponentGeneratorTest < Rails::Generators::TestCase 7 | destination File.join(Rails.root, "tmp", "component_generator_test_output") 8 | setup :prepare_destination 9 | tests React::Generators::ComponentGenerator 10 | 11 | if ShakapackerHelpers.available? 12 | def filename 13 | "app/javascript/components/GeneratedComponent.tsx" 14 | end 15 | else 16 | def filename 17 | "app/assets/javascripts/components/generated_component.es6.tsx" 18 | end 19 | end 20 | 21 | def component_name 22 | "GeneratedComponent" 23 | end 24 | 25 | test "uses ts and es6 syntax" do 26 | run_generator %w[GeneratedComponent name:string --ts --es6] 27 | 28 | assert_file filename, /const #{component_name} = \(props: I#{component_name}Props\) => {/ 29 | end 30 | 31 | test "defines props type" do 32 | run_generator %w[GeneratedComponent name:string --ts --es6] 33 | 34 | assert_file filename, /name: string;/ 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/helper_files/TodoListWithUpdates.js: -------------------------------------------------------------------------------- 1 | var React = require("react") 2 | var createReactClass = require("create-react-class") 3 | 4 | module.exports = createReactClass({ 5 | render: function() { 6 | return ( 7 |
      8 |
    • Updated
    • 9 |
    10 | ) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /test/helper_files/TodoListWithUpdates.js.jsx: -------------------------------------------------------------------------------- 1 | TodoList = createReactClass({ 2 | getInitialState: function() { 3 | return({mounted: "nope"}); 4 | }, 5 | componentWillMount: function() { 6 | this.setState({mounted: 'yep'}); 7 | }, 8 | render: function() { 9 | return ( 10 |
      11 |
    • Updated
    • 12 |
    • {this.state.mounted}
    • 13 | {this.props.todos.map(function(todo, i) { 14 | return () 15 | })} 16 |
    17 | ) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /test/helper_files/WithoutSprockets.js: -------------------------------------------------------------------------------- 1 | // compiled with Babel 6.14.0 2 | // const WithoutSprockets = ({label}) => {label}; 3 | 4 | var WithoutSprockets = function WithoutSprockets(_ref) { 5 | var label = _ref.label; 6 | return React.createElement( 7 | "span", 8 | null, 9 | label 10 | ); 11 | }; -------------------------------------------------------------------------------- /test/react/jsx/jsx_prepocessor_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class JSXPreprocessorTest < ActiveSupport::TestCase 6 | SprocketsHelpers.when_available do 7 | required_javascript = "var requirePlainJavascript = true;" 8 | required_coffeescript = "var requireCoffee; requireCoffee = true;" 9 | required_jsx = 'React.createElement("div", { className: "require-jsx" });' 10 | own_jsx = 'React.createElement("div", { className: "le-javascript" });' 11 | test "executes //= require directives" do # rubocop:disable Minitest/MultipleAssertions 12 | require_parent = SprocketsHelpers.fetch_asset_body("require_test/jsx_preprocessor_test.js") 13 | 14 | assert_compiled_javascript_includes(require_parent, required_javascript) 15 | assert_compiled_javascript_includes(require_parent, required_coffeescript) 16 | assert_compiled_javascript_includes(require_parent, required_jsx) 17 | assert_compiled_javascript_includes(require_parent, own_jsx) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/react/jsx/jsx_transformer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class JSXTransformerTest < ActionDispatch::IntegrationTest 6 | SprocketsHelpers.when_available do 7 | setup do 8 | reset_transformer 9 | React::JSX.transformer_class = React::JSX::JSXTransformer 10 | SprocketsHelpers.manually_expire_asset("JSXTransformer.js") 11 | end 12 | 13 | teardown do 14 | reset_transformer 15 | SprocketsHelpers.manually_expire_asset("JSXTransformer.js") 16 | end 17 | 18 | test "can use dropped-in version of JSX transformer" do 19 | hidden_path = Rails.root.join("vendor/assets/react/JSXTransformer__.js") 20 | replacing_path = Rails.root.join("vendor/assets/react/JSXTransformer.js") 21 | 22 | FileUtils.cp hidden_path, replacing_path 23 | SprocketsHelpers.manually_expire_asset("example3.js") 24 | 25 | get "/assets/example3.js" 26 | FileUtils.rm replacing_path 27 | 28 | assert_response :success 29 | assert_equal "test_confirmation_token_jsx_transformed;", @response.body.strip 30 | end 31 | 32 | test "accepts harmony: true option" do # rubocop:disable Minitest/MultipleAssertions 33 | React::JSX.transform_options = { harmony: true } 34 | get "/assets/harmony_example.js" 35 | 36 | assert_response :success 37 | assert_match(/generateGreeting:\s*function\(\)/, @response.body, "object literal methods") 38 | assert_match(/React.__spread/, @response.body, "spreading props") 39 | assert_match(/Your greeting is: '" \+ insertedGreeting \+ "'/, @response.body, "string interpolation") 40 | assert_match(/active=\$__0\.active/, @response.body, "destructuring assignment") 41 | end 42 | 43 | test "accepts strip_types: true option" do 44 | React::JSX.transform_options = { strip_types: true, harmony: true } 45 | get "/assets/flow_types_example.js" 46 | 47 | assert_response :success 48 | assert_match(/\(i\s*,\s*name\s*\)\s*\{/, @response.body, "type annotations are removed") 49 | end 50 | 51 | test "accepts asset_path: option" do 52 | hidden_path = Rails.root.join("vendor/assets/react/JSXTransformer__.js") 53 | custom_path = Rails.root.join("vendor/assets/react/custom") 54 | replacing_path = custom_path.join("CustomTransformer.js") 55 | 56 | React::JSX.transform_options = { asset_path: "custom/CustomTransformer.js" } 57 | 58 | FileUtils.mkdir_p(custom_path) 59 | FileUtils.cp(hidden_path, replacing_path) 60 | SprocketsHelpers.manually_expire_asset("example3.js") 61 | get "/assets/example3.js" 62 | 63 | FileUtils.rm_rf custom_path 64 | 65 | assert_response :success 66 | assert_equal "test_confirmation_token_jsx_transformed;", @response.body.strip 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/react/jsx_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | # Sprockets is inserting a newline after the docblock for some reason... 6 | EXPECTED_JS = <<~STR 7 | [2].concat([1]);React.createElement("div", null); 8 | STR 9 | 10 | EXPECTED_JS_2 = <<~STR 11 | (function() { 12 | var Component; 13 | 14 | Component = createReactClass({ 15 | render: function() { 16 | return React.createElement(ExampleComponent, {videos:this.props.videos} ); 17 | } 18 | }); 19 | 20 | this.Component = Component; 21 | }).call(this); 22 | STR 23 | 24 | class NullTransformer 25 | def initialize(_options = {}); end # rubocop:disable Style/RedundantInitialize 26 | 27 | def transform(_code) 28 | "TRANSFORMED CODE!;\n" 29 | end 30 | end 31 | 32 | class JSXTransformTest < ActionDispatch::IntegrationTest 33 | SprocketsHelpers.when_available do 34 | setup do 35 | reset_transformer 36 | end 37 | 38 | teardown do 39 | reset_transformer 40 | end 41 | 42 | test "asset pipeline should transform JSX" do 43 | SprocketsHelpers.manually_expire_asset("example.js") 44 | get "/assets/example.js" 45 | 46 | assert_response :success 47 | assert_compiled_javascript_matches(EXPECTED_JS, @response.body) 48 | end 49 | 50 | test "asset pipeline should transform JSX + Coffeescript" do 51 | SprocketsHelpers.manually_expire_asset("example2.js") 52 | get "/assets/example2.js" 53 | 54 | assert_response :success 55 | assert_compiled_javascript_matches(EXPECTED_JS_2, @response.body) 56 | end 57 | 58 | test "use a custom transformer" do 59 | React::JSX.transformer_class = NullTransformer 60 | SprocketsHelpers.manually_expire_asset("example2.js") 61 | get "/assets/example2.js" 62 | 63 | assert_equal "TRANSFORMED CODE!;\n", @response.body 64 | end 65 | 66 | def test_babel_transformer_accepts_babel_transformation_options 67 | React::JSX.transform_options = { blacklist: ["spec.functionName", "validation.react", "strict"] } 68 | SprocketsHelpers.manually_expire_asset("example.js") 69 | get "/assets/example.js" 70 | 71 | assert_response :success 72 | 73 | refute_includes @response.body, "strict" 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/react/rails/asset_variant_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class AssetVariantTest < ActiveSupport::TestCase 6 | def build_variant(options) 7 | React::Rails::AssetVariant.new(options) 8 | end 9 | 10 | test "it points to different directories for react" do 11 | production_variant = build_variant(variant: :production) 12 | 13 | assert_match(%r{/lib/assets/react-source/production}, production_variant.react_directory) 14 | 15 | development_variant = build_variant(variant: nil) 16 | 17 | assert_match(%r{/lib/assets/react-source/development}, development_variant.react_directory) 18 | end 19 | 20 | test "points to jsx transformer" do 21 | variant = build_variant({}) 22 | 23 | assert_match(%r{/lib/assets/javascripts/}, variant.jsx_directory) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/react/rails/controller_lifecycle_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | # This helper implementation just counts the number of 6 | # calls to `react_component` 7 | class DummyHelperImplementation 8 | attr_reader :events 9 | 10 | def initialize 11 | @events = [] 12 | end 13 | 14 | def setup(controller) 15 | @events << (controller.params["param_test"] || :setup) 16 | end 17 | 18 | def teardown(_env) 19 | @events << :teardown 20 | end 21 | 22 | def react_component(*_args) 23 | @events << :react_component 24 | end 25 | end 26 | 27 | class ControllerLifecycleTest < ActionDispatch::IntegrationTest 28 | compiled = false 29 | setup do 30 | ShakapackerHelpers.compile unless compiled 31 | 32 | @previous_helper_implementation = React::Rails::ViewHelper.helper_implementation_class 33 | React::Rails::ViewHelper.helper_implementation_class = DummyHelperImplementation 34 | end 35 | 36 | def teardown 37 | React::Rails::ViewHelper.helper_implementation_class = @previous_helper_implementation 38 | end 39 | 40 | test "it creates a helper object and puts it in the request env" do 41 | get "/pages/1" 42 | helper_obj = controller.__react_component_helper 43 | 44 | assert_kind_of(DummyHelperImplementation, helper_obj, "It uses the view helper implementation class") 45 | end 46 | 47 | test "it calls setup and teardown methods" do 48 | get "/pages/1?param_test=123" 49 | helper_obj = controller.__react_component_helper 50 | lifecycle_steps = ["123", :react_component, :react_component, :teardown] 51 | 52 | assert_equal(lifecycle_steps, helper_obj.events) 53 | end 54 | 55 | test "there's a new helper object for every request" do 56 | get "/pages/1" 57 | first_helper = controller.__react_component_helper 58 | get "/pages/1" 59 | second_helper = controller.__react_component_helper 60 | 61 | refute_equal(first_helper, second_helper, "The helper for the second request is brand new") 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/react/rails/pages_controller_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class PagesControllerTest < ActionController::TestCase 6 | include ParamsHelper 7 | setup do 8 | ShakapackerHelpers.compile_if_missing 9 | end 10 | 11 | test "renders successfully" do 12 | get :show, params: { id: 1 } 13 | 14 | assert_equal(200, response.status) 15 | end 16 | 17 | when_stateful_js_context_available do 18 | test "it sets up and tears down a react context" do 19 | get :show, params: { id: 1, prerender: true } 20 | 21 | assert_includes(response.body, "Hello") 22 | end 23 | 24 | test "it sets up and tears down a react context with the given greeting text" do 25 | get :show, params: { id: 1, prerender: true, greeting: "Howdy" } 26 | 27 | assert_includes(response.body, "Howdy") 28 | end 29 | 30 | test "it sets up and tears down a react context with the given greeting emoji" do 31 | get :show, params: { id: 1, prerender: true, greeting: "👋" } 32 | 33 | assert_includes(response.body, "👋") 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/react/rails/railtie_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RailtieTest < ActionDispatch::IntegrationTest 6 | test "reloaders are configured after initializers are loaded" do 7 | @test_file = File.expand_path("../../dummy/app/pants/yfronts.js", File.dirname(__FILE__)) 8 | FileUtils.touch @test_file 9 | results = Dummy::Application.reloaders.map(&:updated?) 10 | 11 | assert_includes(results, true) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/react/rails/realtime_update_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class RealtimeUpdateTest < ActiveSupport::TestCase 6 | ShakapackerHelpers.when_shakapacker_available do 7 | include Capybara::DSL 8 | 9 | def assert_counter_count(page, timer_name, count) 10 | assert page.has_content?("#{timer_name} - #{count}"), <<~MSG 11 | #{page.body} 12 | #{page.driver.browser.logs.get(:browser).inspect} 13 | MSG 14 | end 15 | 16 | setup do 17 | Capybara.current_driver = Capybara.javascript_driver 18 | ShakapackerHelpers.compile 19 | React::ServerRendering.reset_pool 20 | end 21 | 22 | teardown do 23 | ShakapackerHelpers.clear_shakapacker_packs 24 | end 25 | 26 | test "doesn't re-mount the components trees when mountComponents is called" do 27 | visit "/counters" 28 | 29 | assert_counter_count(page, "Counter 1", 0) 30 | page.click_button "Increment Counter 1" 31 | page.click_button "Add counter" 32 | sleep 0.1 33 | 34 | assert_counter_count(page, "Counter 1", 1) 35 | assert_counter_count(page, "Counter 2", 0) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/react/rails/test_helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class TestHelperTest < ActionDispatch::IntegrationTest 6 | setup do 7 | ShakapackerHelpers.compile_if_missing 8 | end 9 | 10 | test "assert_react_component" do # rubocop:disable Minitest/MultipleAssertions 11 | get "/pages/1" 12 | 13 | assert_equal 200, response.status 14 | assert_react_component "GreetingMessage" 15 | assert_react_component "GreetingMessage" do |props| 16 | assert_equal "Bob", props[:name] 17 | assert_equal "Last Bob", props[:lastName] 18 | assert_equal "Bob", props[:info][:name] 19 | assert_equal "Last Bob", props[:info][:lastName] 20 | 21 | assert_select "[id=?]", "component" 22 | assert_select "[class=?]", "greeting-message" 23 | end 24 | assert_react_component "Todo" do |props| 25 | assert_equal "Another Component", props[:todo] 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/react/rails/view_helper_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | # Provide direct access to the view helper methods 6 | class ViewHelperHelper 7 | extend ActionView::Context 8 | extend ActionView::Helpers::CaptureHelper 9 | extend React::Rails::ViewHelper 10 | end 11 | 12 | class ViewHelperTest < ActionView::TestCase 13 | test "view helper can be called directly" do 14 | expected_html = %(
    ) # rubocop:disable Layout/LineLength 15 | rendered_html = ViewHelperHelper.react_component("Component", { a: "b" }) 16 | 17 | assert_equal(expected_html, rendered_html) 18 | end 19 | 20 | test "view helper accepts block usage" do 21 | expected_html = %(
    content
    ) # rubocop:disable Layout/LineLength 22 | rendered_html = ViewHelperHelper.react_component("Component", { a: "b" }) do 23 | "content" 24 | end 25 | 26 | assert_equal(expected_html, rendered_html) 27 | end 28 | 29 | test "view helper can be used in stand-alone views" do 30 | @name = "React-Rails" 31 | render template: "pages/show" 32 | 33 | assert_includes(rendered, "React-Rails") 34 | end 35 | 36 | test "view helper can accept block and render inner content only once" do 37 | rendered_html = render partial: "pages/component_with_inner_html" 38 | expected_html = <<~HTML 39 |
    40 |
    NestedContent
    41 |
    42 | HTML 43 | assert_equal expected_html.strip, rendered_html 44 | end 45 | 46 | test "view helper uses the implementation class set in the initializer" do 47 | assert_equal("CustomComponentMount", React::Rails::ViewHelper.helper_implementation_class.to_s) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /test/react/rails/webpacker_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ReactRailsShakapackerTest < ActionDispatch::IntegrationTest 6 | ShakapackerHelpers.when_shakapacker_available do 7 | include Capybara::DSL 8 | 9 | setup do 10 | Capybara.current_driver = Capybara.javascript_driver 11 | ShakapackerHelpers.compile 12 | React::ServerRendering.reset_pool 13 | end 14 | 15 | teardown do 16 | ShakapackerHelpers.clear_shakapacker_packs 17 | end 18 | 19 | test "it mounts components from the pack" do # rubocop:disable Minitest/MultipleAssertions 20 | visit "/pack_component" 21 | 22 | assert page.has_content?("Export Default") 23 | assert page.has_content?("Named Export") 24 | assert page.has_content?("Exports") 25 | assert page.has_content?("Global Component") 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/react/server_rendering/console_replay_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | if ShakapackerHelpers.available? || SprocketsHelpers.available? 6 | class ConsoleReplayTest < ActionDispatch::IntegrationTest 7 | setup do 8 | ShakapackerHelpers.compile 9 | React::ServerRendering.renderer_options = { replay_console: true } 10 | React::ServerRendering.reset_pool 11 | end 12 | 13 | EXPECTED_REPLAY = <<~HTML 14 | 17 | HTML 18 | 19 | test "it clears the state between each request" do 20 | # Each request should only contain one log: 21 | get "/server/1" 22 | 23 | assert_includes(response.body, EXPECTED_REPLAY) 24 | get "/server/1" 25 | 26 | assert_includes(response.body, EXPECTED_REPLAY) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/react/server_rendering/exec_js_renderer_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | DUMMY_IMPLEMENTATION = " 6 | var Todo = null 7 | var React = { 8 | createElement: function() {}, 9 | } 10 | this.ReactRailsUJS = { 11 | serverRender: function() { 12 | return 'serverRender was called' 13 | }, 14 | } 15 | " 16 | 17 | class ExecJSRendererTest < ActiveSupport::TestCase 18 | setup do 19 | react_server_source = File.read(File.expand_path("../../../lib/assets/react-source/production/react-server.js", 20 | __dir__)) 21 | react_ujs_source = File.read(File.expand_path("../../../lib/assets/javascripts/react_ujs.js", __dir__)) 22 | todo_component_source = File.read( 23 | File.expand_path( 24 | "../../dummy/app/assets/javascripts/components/PlainJSTodo.js", __dir__ 25 | ) 26 | ) 27 | code = react_server_source + react_ujs_source + todo_component_source 28 | @renderer = React::ServerRendering::ExecJSRenderer.new(code: code) 29 | end 30 | 31 | test "#render returns HTML" do 32 | result = @renderer.render("Todo", { todo: "write tests" }.to_json, {}) 33 | 34 | assert_match(%r{}, result) 35 | end 36 | 37 | test "#render accepts render_function:" do 38 | result = @renderer.render("Todo", { todo: "write more tests" }.to_json, render_function: "renderToStaticMarkup") 39 | 40 | assert_match(%r{
  • write more tests
  • }, result) 41 | end 42 | 43 | test "#before_render is called before #after_render" do 44 | def @renderer.before_render(_name, _props, _opts) 45 | "throw 'before_render ' + afterRenderVar" 46 | end 47 | 48 | def @renderer.after_render(_name, _props, _opts) 49 | "var afterRenderVar = 'assigned_after_render'" 50 | end 51 | 52 | error = assert_raises(React::ServerRendering::PrerenderError) do 53 | @renderer.render("Todo", { todo: "write tests" }.to_json, {}) 54 | end 55 | 56 | assert_match(/before_render/, error.message) 57 | assert_no_match(/assigned_after_render/, error.message) 58 | end 59 | 60 | test "#after_render is called after #before_render" do 61 | def @renderer.before_render(_name, _props, _opts) 62 | "var beforeRenderVar = 'assigned_before_render'" 63 | end 64 | 65 | def @renderer.after_render(_name, _props, _opts) 66 | "throw 'after_render ' + beforeRenderVar" 67 | end 68 | 69 | error = assert_raises(React::ServerRendering::PrerenderError) do 70 | @renderer.render("Todo", { todo: "write tests" }.to_json, {}) 71 | end 72 | 73 | assert_match(/after_render/, error.message) 74 | assert_match(/assigned_before_render/, error.message) 75 | end 76 | 77 | test ".new accepts code:" do 78 | dummy_renderer = React::ServerRendering::ExecJSRenderer.new(code: DUMMY_IMPLEMENTATION) 79 | result = dummy_renderer.render("Todo", { todo: "get a real job" }.to_json, {}) 80 | 81 | assert_equal("serverRender was called", result) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/react/server_rendering/manifest_container_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | # sprockets-rails < 2.2.2 does not support 6 | # `application.assets_manifest`. Since sprockets-rails < 2.1.2 does 7 | # not define `Sprockets::Rails::VERSION`, checking for 8 | # `Sprockets::Rails` is not enough. 9 | if defined?(Sprockets::Rails::VERSION) && 10 | Gem::Version.new(Sprockets::Rails::VERSION) >= Gem::Version.new("2.2.2") 11 | 12 | class ManifestContainerTest < ActiveSupport::TestCase 13 | def setup 14 | SprocketsHelpers.precompile_assets 15 | 16 | @manifest_container = React::ServerRendering::ManifestContainer.new 17 | end 18 | 19 | def teardown 20 | SprocketsHelpers.clear_precompiled_assets 21 | end 22 | 23 | def test_find_asset_gets_asset_contents 24 | application_js_content = @manifest_container.find_asset("application.js") 25 | 26 | assert_operator(application_js_content.length, :>, 50_000, "It's the compiled file") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/react/server_rendering/webpacker_containers_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | require "open-uri" 5 | 6 | class ShakapackerManifestContainerTest < ActiveSupport::TestCase 7 | ShakapackerHelpers.when_shakapacker_available do 8 | setup do 9 | ShakapackerHelpers.clear_shakapacker_packs 10 | end 11 | 12 | def test_it_loads_js_from_the_shakapacker_container 13 | ShakapackerHelpers.compile 14 | container = React::ServerRendering::SeparateServerBundleContainer.new 15 | 16 | assert_not_empty container.find_asset("server_rendering.js") 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/react/server_rendering/yaml_manifest_container_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | if Rails::VERSION::MAJOR == 3 6 | class YamlManifestContainerTest < ActiveSupport::TestCase 7 | def setup 8 | SprocketsHelpers.precompile_assets 9 | 10 | @manifest_container = React::ServerRendering::YamlManifestContainer.new 11 | end 12 | 13 | def teardown 14 | SprocketsHelpers.clear_precompiled_assets 15 | end 16 | 17 | def test_find_asset_gets_asset_contents 18 | application_js_content = @manifest_container.find_asset("application.js") 19 | 20 | assert_operator(application_js_content.length, :>, 50_000, "It's the compiled file") 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/react/server_rendering_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class NullRenderer 6 | def initialize(options) 7 | # in this case, options is actually a string (just for testing) 8 | @name = options 9 | end 10 | 11 | def render(component_name, props, prerender_options) 12 | "#{@name} rendered #{component_name} with #{props} and #{prerender_options}" 13 | end 14 | end 15 | 16 | class ReactServerRenderingTest < ActiveSupport::TestCase 17 | setup do 18 | @previous_renderer = React::ServerRendering.renderer 19 | @previous_options = React::ServerRendering.renderer_options 20 | React::ServerRendering.renderer_options = "TEST" 21 | React::ServerRendering.renderer = NullRenderer 22 | React::ServerRendering.reset_pool 23 | end 24 | 25 | teardown do 26 | React::ServerRendering.renderer = @previous_renderer 27 | React::ServerRendering.renderer_options = @previous_options 28 | React::ServerRendering.reset_pool 29 | end 30 | 31 | test ".render returns a rendered string" do 32 | props = { "props" => true } 33 | result = React::ServerRendering.render("MyComponent", props, "prerender-opts") 34 | 35 | assert_equal("TEST rendered MyComponent with #{props} and prerender-opts", result) 36 | end 37 | 38 | test ".reset_pool forgets old renderers" do # rubocop:disable Minitest/MultipleAssertions 39 | # At first, they use the first options: 40 | assert_match(/^TEST/, React::ServerRendering.render(nil, nil, nil)) 41 | assert_match(/^TEST/, React::ServerRendering.render(nil, nil, nil)) 42 | 43 | # Then change the init options and clear the pool: 44 | React::ServerRendering.renderer_options = "DIFFERENT" 45 | React::ServerRendering.reset_pool 46 | # New renderers are created with the new init options: 47 | assert_match(/^DIFFERENT/, React::ServerRendering.render(nil, nil, nil)) 48 | assert_match(/^DIFFERENT/, React::ServerRendering.render(nil, nil, nil)) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/react_asset_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ReactAssetTest < ActionDispatch::IntegrationTest 6 | SprocketsHelpers.when_available do 7 | setup do 8 | SprocketsHelpers.clear_sprockets_cache 9 | end 10 | 11 | teardown do 12 | SprocketsHelpers.clear_sprockets_cache 13 | end 14 | 15 | test "asset pipeline should deliver drop-in react file replacement" do 16 | app_react_file_path = File.expand_path("dummy/vendor/assets/javascripts/react.js", __dir__) 17 | react_file_token = "'test_confirmation_token_react_content_non_production';\n" 18 | File.write(app_react_file_path, react_file_token) 19 | SprocketsHelpers.manually_expire_asset("react.js") 20 | react_asset = Rails.application.assets["react.js"] 21 | 22 | get "/assets/react.js" 23 | 24 | File.unlink(app_react_file_path) 25 | 26 | assert_response :success 27 | assert_equal react_file_token.length, react_asset.to_s.length, "The asset pipeline serves the drop-in file" 28 | assert_equal react_file_token.length, @response.body.length, "The asset route serves the drop-in file" 29 | end 30 | 31 | test "precompiling assets works" do 32 | SprocketsHelpers.precompile_assets 33 | ensure 34 | SprocketsHelpers.clear_precompiled_assets 35 | end 36 | 37 | test "the production build is optimized for production" do 38 | production_path = File.expand_path("../lib/assets/react-source/production/react.js", __dir__) 39 | production_js = File.read(production_path) 40 | env_checks = production_js.scan("NODE_ENV") 41 | 42 | assert_equal(0, env_checks.length, "Dead code is removed for production") 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/react_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "test_helper" 4 | 5 | class ReactTest < ActiveSupport::TestCase 6 | def test_it_camelizes_props 7 | raw_props = { 8 | multi_word_sym: { 9 | nested_key: [ 10 | { double_nested: true }, 11 | 1, 12 | "string item", 13 | [{ nested_array: {} }] 14 | ] 15 | }, 16 | "alreadyCamelized" => :ok 17 | } 18 | 19 | expected_props = { 20 | "multiWordSym" => { 21 | "nestedKey" => [ 22 | { "doubleNested" => true }, 23 | 1, 24 | "string item", 25 | [{ "nestedArray" => {} }] 26 | ] 27 | }, 28 | "alreadyCamelized" => "ok" 29 | } 30 | 31 | assert_equal expected_props, React.camelize_props(raw_props) 32 | end 33 | 34 | def test_it_camelizes_params 35 | raw_params = ActionController::Parameters.new({ 36 | foo_bar_baz: "foo bar baz", 37 | nested_keys: { 38 | qux_etc: "bish bash bosh" 39 | } 40 | }) 41 | permitted_params = raw_params.permit(:foo_bar_baz, nested_keys: :qux_etc) 42 | 43 | expected_params = { 44 | "fooBarBaz" => "foo bar baz", 45 | "nestedKeys" => { 46 | "quxEtc" => "bish bash bosh" 47 | } 48 | } 49 | 50 | assert_equal expected_params, React.camelize_props(permitted_params) 51 | end 52 | 53 | def test_it_camelizes_json_serializable_objects 54 | my_json_serializer = Class.new do 55 | def initialize(data) 56 | @data = data 57 | end 58 | 59 | def as_json 60 | @data 61 | end 62 | end 63 | 64 | raw_props = { 65 | key_one: "value1", 66 | key_two: my_json_serializer.new( 67 | nested_key_one: "nested_value1", 68 | nested_key_two: %w[nested value two] 69 | ) 70 | } 71 | 72 | expected_params = { 73 | "keyOne" => "value1", 74 | "keyTwo" => { 75 | "nestedKeyOne" => "nested_value1", 76 | "nestedKeyTwo" => %w[nested value two] 77 | } 78 | } 79 | 80 | assert_equal expected_params, React.camelize_props(raw_props) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/support/sprockets_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module SprocketsHelpers 4 | module_function 5 | 6 | def available? 7 | # We can't scan for sprockets in gemfile_lock because it's 8 | # a dependency of Rails even if not required. 9 | # We also can't scan for defined?(Sprockets) because this is used to 10 | # require Sprockets in the config/application.rb 11 | # !!Bundler.locked_gems.specs.find {|gem_spec| gem_spec.name == 'sprockets'} 12 | ENV.fetch("BUNDLE_GEMFILE", nil).include?("sprockets") 13 | end 14 | 15 | # The block depends on sprockets, don't run it if sprockets is missing 16 | def when_available 17 | return unless available? 18 | 19 | yield 20 | end 21 | 22 | def clear_sprockets_cache 23 | # Remove cached files 24 | Rails.root.join("tmp/cache").tap do |tmp| 25 | tmp.rmtree if tmp.exist? 26 | tmp.mkpath 27 | end 28 | end 29 | 30 | def fetch_asset_body(asset_logical_path) 31 | Rails.application.assets[asset_logical_path].to_s 32 | end 33 | 34 | # Sprockets 2 doesn't expire this assets well in 35 | # this kind of setting, 36 | # so override `fresh?` to mark it as expired. 37 | def manually_expire_asset(asset_name) 38 | asset = Rails.application.assets[asset_name] 39 | def asset.fresh?(_env) 40 | false 41 | end 42 | end 43 | 44 | def precompile_assets 45 | invoke_assets_precompile_task 46 | 47 | if Rails.application.respond_to?(:assets_manifest) 48 | # Make a new manifest since assets weren't compiled before 49 | config = Rails.application.config 50 | path = File.join(config.paths["public"].first, config.assets.prefix) 51 | new_manifest = Sprockets::Manifest.new(Rails.application.assets, path) 52 | Rails.application.assets_manifest = new_manifest 53 | end 54 | 55 | assets_directory = File.expand_path("../dummy/public/assets", __dir__) 56 | raise "Asset precompilation failed" unless Dir.exist?(assets_directory) 57 | end 58 | 59 | def clear_precompiled_assets 60 | assets_directory = File.expand_path("../dummy/public/assets", __dir__) 61 | FileUtils.rm_r(assets_directory) 62 | ENV.delete("RAILS_GROUPS") 63 | end 64 | 65 | class << self 66 | private 67 | 68 | def invoke_assets_precompile_task 69 | # Changing directories is required because: 70 | # - assets:precompile runs webpacker:compile when availabled 71 | # - webpacker:compile depends on `./bin/webpack`, so `.` must be the app root 72 | Dir.chdir("./test/dummy") do 73 | ENV["RAILS_GROUPS"] = "assets" # required for Rails 3.2 74 | Rake::Task["assets:precompile"].reenable 75 | 76 | if Rails::VERSION::MAJOR == 3 77 | Rake::Task["assets:precompile:all"].reenable 78 | Rake::Task["assets:precompile:primary"].reenable 79 | Rake::Task["assets:precompile:nondigest"].reenable 80 | end 81 | 82 | Rake::Task["assets:precompile"].invoke 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /test/support/webpacker_helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ShakapackerHelpers 4 | PACKS_DIRECTORY = File.expand_path("../dummy/public/packs", __dir__) 5 | 6 | module_function 7 | 8 | def available? 9 | !!defined?(Shakapacker) 10 | end 11 | 12 | def when_shakapacker_available 13 | return unless available? 14 | 15 | yield 16 | end 17 | 18 | def compile 19 | return unless available? 20 | 21 | clear_shakapacker_packs 22 | Dir.chdir("./test/dummy") do 23 | Rake::Task["shakapacker:compile"].reenable 24 | Rake::Task["shakapacker:compile"].invoke 25 | end 26 | # Reload cached JSON manifest: 27 | manifest_refresh 28 | end 29 | 30 | def compile_if_missing 31 | return if File.exist?(PACKS_DIRECTORY) 32 | 33 | compile 34 | end 35 | 36 | def clear_shakapacker_packs 37 | FileUtils.rm_rf(PACKS_DIRECTORY) 38 | end 39 | 40 | # Start a webpack-dev-server 41 | # Call the block 42 | # Make sure to clean up the server 43 | def with_dev_server 44 | old_env = ENV.fetch("NODE_ENV", nil) 45 | ENV["NODE_ENV"] = "development" 46 | 47 | # Start the server in a forked process: 48 | Dir.chdir("test/dummy") do 49 | spawn "RAILS_ENV=development ./bin/shakapacker-dev-server" 50 | end 51 | 52 | stop_time = Time.now + 30.seconds 53 | detected_dev_server = false 54 | loop do 55 | detected_dev_server = dev_server_running? 56 | break if detected_dev_server || Time.now > stop_time 57 | 58 | sleep 0.5 59 | end 60 | 61 | # If we didn't hook up with a dev server after waiting, fail loudly. 62 | raise "Failed to start dev server" unless detected_dev_server 63 | 64 | puts "Detected dev server - Continuing" 65 | 66 | # Call the test block: 67 | yield 68 | ensure 69 | check_cmd = "lsof -i :8080 -S" 70 | 10.times do 71 | # puts check_cmd 72 | status = `#{check_cmd}` 73 | # puts status 74 | remaining_pid_match = status.match(/\n[a-z]+\s+(\d+)/) 75 | break unless remaining_pid_match 76 | 77 | remaining_pid = remaining_pid_match[1] 78 | # puts "Remaining #{remaining_pid}" 79 | kill_cmd = "kill -9 #{remaining_pid}" 80 | # puts kill_cmd 81 | `#{kill_cmd}` 82 | sleep 0.5 83 | end 84 | 85 | # Remove the dev-server packs: 86 | ShakapackerHelpers.clear_shakapacker_packs 87 | ENV["NODE_ENV"] = old_env 88 | puts "Killed." 89 | end 90 | 91 | def dev_server_running? 92 | Shakapacker.instance.instance_variable_set(:@config, nil) 93 | return false unless Shakapacker.dev_server.running? 94 | 95 | ds = Shakapacker.dev_server 96 | example_asset_path = manifest_data.values.first 97 | return false unless example_asset_path 98 | 99 | begin 100 | file = URI.parse("#{ds.protocol}://#{ds.host}:#{ds.port}#{example_asset_path}").open 101 | rescue StandardError 102 | file = nil 103 | end 104 | unless file 105 | puts "Dev server is not serving assets yet" 106 | return false 107 | end 108 | true 109 | end 110 | 111 | def manifest_refresh 112 | Shakapacker.manifest.refresh 113 | end 114 | 115 | def manifest_lookup 116 | Shakapacker.manifest 117 | end 118 | 119 | def manifest_data 120 | Shakapacker.manifest.refresh 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | if RUBY_PLATFORM != "java" 4 | require "simplecov" 5 | SimpleCov.start 6 | end 7 | 8 | support_path = File.expand_path("support/*.rb", __dir__) 9 | Dir.glob(support_path).sort.each do |f| 10 | require(f) 11 | end 12 | 13 | # Configure Rails Environment 14 | ENV["RAILS_ENV"] = "test" 15 | 16 | require File.expand_path("dummy/config/environment.rb", __dir__) 17 | require "rails/test_help" 18 | require "rails/generators" 19 | require "pathname" 20 | require "minitest/mock" 21 | require "capybara/rails" 22 | require "selenium/webdriver" 23 | require "minitest/retry" 24 | Minitest::Retry.use! 25 | Dummy::Application.load_tasks 26 | 27 | ShakapackerHelpers.clear_shakapacker_packs 28 | 29 | Capybara.app = Rails.application 30 | Capybara.server = :webrick 31 | 32 | Capybara.register_driver :headless_chrome do |app| 33 | options = Selenium::WebDriver::Chrome::Options.new(args: %w[no-sandbox headless=new disable-gpu]) 34 | 35 | Capybara::Selenium::Driver.new(app, browser: :chrome, options: options, timeout: 300) 36 | end 37 | 38 | Capybara.javascript_driver = :headless_chrome 39 | Capybara.current_driver = Capybara.javascript_driver 40 | 41 | CACHE_PATH = Pathname.new File.expand_path("dummy/tmp/cache", __dir__) 42 | 43 | Rails.backtrace_cleaner.remove_silencers! 44 | 45 | def reset_transformer 46 | SprocketsHelpers.clear_sprockets_cache 47 | React::JSX.transformer_class = React::JSX::DEFAULT_TRANSFORMER 48 | React::JSX.transform_options = {} 49 | React::JSX.transformer = nil 50 | end 51 | 52 | # Load support files 53 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f } 54 | 55 | # Load fixtures from the engine 56 | if ActiveSupport::TestCase.method_defined?(:fixture_path=) 57 | ActiveSupport::TestCase.fixture_path = File.expand_path("fixtures", __dir__) 58 | end 59 | 60 | ActiveSupport::TestCase.test_order = :random if ActiveSupport::TestCase.respond_to?(:test_order=) 61 | 62 | def wait_for_turbolinks_to_be_available 63 | sleep(1) 64 | end 65 | 66 | # Different processors may generate slightly different outputs, 67 | # as some version inserts an extra "\n" at the beginning. 68 | # Because appraisal is used, multiple versions of coffee-script are treated 69 | # together. Remove all spaces to make test pass. 70 | def assert_compiled_javascript_matches(javascript, expectation) 71 | assert_equal expectation.gsub(/\s/, ""), javascript.gsub(/\s/, "") 72 | end 73 | 74 | def assert_compiled_javascript_includes(javascript, expected_part) 75 | assert_includes javascript.gsub(/\s/, ""), expected_part.gsub(/\s/, "") 76 | end 77 | 78 | def when_stateful_js_context_available 79 | return unless defined?(V8) || defined?(MiniRacer) 80 | 81 | yield 82 | end 83 | 84 | def expected_working_jsx 85 | /\.createElement\(\s*\S*\.Fragment,\s*null,\s*"Name:\s*",\s*this\.props\.name,\s*"Address:\s*",\s*this\.props\.address\s*\)/x # rubocop:disable Layout/LineLength 86 | end 87 | 88 | def expected_working_jsx_in_function_component 89 | /\.createElement\(\s*\S*\.Fragment,\s*null,\s*"Name:\s*",\s*props\.name,\s*"Address:\s*",\s*props\.address\s*\)/x 90 | end 91 | 92 | module ParamsHelper 93 | # Normalize params for Rails 5.1+ 94 | def query_params(params) 95 | if Rails::VERSION::MAJOR > 4 96 | { params: params } 97 | else 98 | params 99 | end 100 | end 101 | end 102 | --------------------------------------------------------------------------------