├── .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()(\w+)>$}m, '\2>\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 |
12 | - {this.state.mounted}
13 | {this.props.todos.map(function(todo, i) {
14 | return ()
15 | })}
16 |
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 |
15 | - Console Logged
16 | - {this.state.mounted}
17 | {this.props.todos.map(function(todo, i) {
18 | return ()
19 | })}
20 |
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 |
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 |
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 |
--------------------------------------------------------------------------------