├── .github └── workflows │ ├── dynamic-readme.yml │ ├── dynamic-security.yml │ └── main.yml ├── .gitignore ├── .ruby-version ├── .standard.yml ├── CODEOWNERS ├── CONTRIBUTING.md ├── FEATURES.md ├── GOALS.md ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── NEWS.md ├── README.md ├── RELEASING.md ├── Rakefile ├── SECURITY.md ├── bin ├── rails ├── setup └── test ├── lib ├── generators │ ├── suspenders │ │ ├── accessibility_generator.rb │ │ ├── advisories_generator.rb │ │ ├── ci_generator.rb │ │ ├── email_generator.rb │ │ ├── environments │ │ │ ├── development_generator.rb │ │ │ ├── production_generator.rb │ │ │ └── test_generator.rb │ │ ├── factories_generator.rb │ │ ├── inline_svg_generator.rb │ │ ├── install │ │ │ └── web_generator.rb │ │ ├── jobs_generator.rb │ │ ├── lint_generator.rb │ │ ├── prerequisites_generator.rb │ │ ├── rake_generator.rb │ │ ├── setup_generator.rb │ │ ├── styles_generator.rb │ │ ├── tasks_generator.rb │ │ ├── testing_generator.rb │ │ └── views_generator.rb │ └── templates │ │ ├── ci │ │ └── ci.yml.tt │ │ ├── email │ │ └── email_interceptor.rb │ │ ├── factories │ │ ├── factories.rb │ │ ├── factories_spec.rb │ │ ├── factories_test.rb │ │ └── factory_bot_rspec.rb │ │ ├── inline_svg │ │ └── inline_svg.rb │ │ ├── install │ │ └── web │ │ │ └── CONTRIBUTING.md │ │ ├── lint │ │ ├── config_better_html.yml │ │ ├── config_initializers_better_html.rb │ │ ├── erb-lint.yml │ │ ├── erblint.rake │ │ ├── eslintrc.json │ │ ├── package.json │ │ ├── prettierignore │ │ ├── prettierrc │ │ ├── rubocop.yml.tt │ │ └── stylelintrc.json │ │ ├── prerequisites │ │ └── node-version.tt │ │ ├── setup │ │ └── bin_setup.rb │ │ ├── styles │ │ └── postcss.config.js │ │ ├── tasks │ │ └── dev.rake │ │ ├── testing │ │ ├── action_mailer.rb │ │ ├── driver.rb │ │ ├── i18n.rb │ │ └── shoulda_matchers.rb │ │ └── views │ │ └── flashes.html.erb ├── install │ └── web.rb ├── suspenders.rb ├── suspenders │ ├── cleanup │ │ ├── generate_readme.rb │ │ └── organize_gemfile.rb │ ├── engine.rb │ ├── generators.rb │ ├── railtie.rb │ └── version.rb └── tasks │ └── suspenders.rake ├── suspenders.gemspec └── test ├── dummy ├── Rakefile ├── app │ ├── assets │ │ ├── images │ │ │ └── .keep │ │ └── stylesheets │ │ │ └── application.css │ ├── channels │ │ └── application_cable │ │ │ ├── channel.rb │ │ │ └── connection.rb │ ├── controllers │ │ ├── application_controller.rb │ │ └── concerns │ │ │ └── .keep │ ├── helpers │ │ └── application_helper.rb │ ├── jobs │ │ └── application_job.rb │ ├── mailers │ │ └── application_mailer.rb │ ├── models │ │ ├── application_record.rb │ │ └── concerns │ │ │ └── .keep │ └── views │ │ └── layouts │ │ ├── application.html.erb │ │ ├── mailer.html.erb │ │ └── mailer.text.erb ├── bin │ ├── dev │ ├── rails │ ├── rake │ ├── rubocop │ ├── setup │ └── thrust ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── cable.yml │ ├── database.yml │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── content_security_policy.rb │ │ ├── filter_parameter_logging.rb │ │ ├── inflections.rb │ │ ├── new_framework_defaults_8_0.rb │ │ └── permissions_policy.rb │ ├── locales │ │ └── en.yml │ ├── puma.rb │ ├── routes.rb │ └── storage.yml ├── db │ ├── migrate │ │ ├── 20241129142620_add_service_name_to_active_storage_blobs.active_storage.rb │ │ ├── 20241129142621_create_active_storage_variant_records.active_storage.rb │ │ └── 20241129142622_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb │ └── schema.rb ├── lib │ └── assets │ │ └── .keep ├── log │ └── .keep ├── public │ ├── 400.html │ ├── 404.html │ ├── 406-unsupported-browser.html │ ├── 422.html │ ├── 500.html │ ├── apple-touch-icon-precomposed.png │ ├── apple-touch-icon.png │ ├── favicon.ico │ ├── icon.png │ ├── icon.svg │ └── robots.txt ├── storage │ └── .keep └── tmp │ ├── .keep │ ├── pids │ └── .keep │ └── storage │ └── .keep ├── fixtures └── files │ ├── Rakefile │ ├── Rakefile_advisories │ ├── _flashes.html.erb │ ├── action_mailer.rb │ ├── better_html.rb │ ├── driver.rb │ ├── email_interceptor.rb │ ├── email_interceptor_initializer.rb │ ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb │ ├── erb-lint.yml │ ├── eslintrc.json │ ├── factories.rb │ ├── factories_spec_lint.rb │ ├── factories_test_lint.rb │ ├── gemfile_clean │ ├── gemfile_messy │ ├── i18n.rb │ ├── inline_svg.rb │ ├── postcss.config.js │ ├── prettierignore │ ├── prettierrc.json │ ├── rails_helper.rb │ ├── shoulda_matchers.rb │ ├── spec_helper.rb │ ├── stylelintrc.json │ └── test_helper.rb ├── generators └── suspenders │ ├── accessibility_generator_test.rb │ ├── advisories_generator_test.rb │ ├── ci_generator_test.rb │ ├── email_generator_test.rb │ ├── environments │ ├── development_generator_test.rb │ ├── production_generator_test.rb │ └── test_generator_test.rb │ ├── factories_generator_test.rb │ ├── inline_svg_generator_test.rb │ ├── install │ └── web_generator_test.rb │ ├── jobs_generator_test.rb │ ├── lint_generator_test.rb │ ├── prerequisites_generator_test.rb │ ├── rake_generator_test.rb │ ├── setup_generator_test.rb │ ├── styles_generator_test.rb │ ├── tasks_generator_test.rb │ ├── testing_generator_test.rb │ └── views_generator_test.rb ├── suspenders ├── cleanup │ ├── generate_readme_test.rb │ └── organize_gemfile_test.rb └── generators_test.rb ├── suspenders_test.rb └── test_helper.rb /.github/workflows/dynamic-readme.yml: -------------------------------------------------------------------------------- 1 | name: update-templates 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - README.md 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-templates: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-readme.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/dynamic-security.yml: -------------------------------------------------------------------------------- 1 | name: update-security 2 | 3 | on: 4 | push: 5 | paths: 6 | - SECURITY.md 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-security: 13 | permissions: 14 | contents: write 15 | pull-requests: write 16 | pages: write 17 | uses: thoughtbot/templates/.github/workflows/dynamic-security.yaml@main 18 | secrets: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Ruby 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | bundler-cache: true 20 | 21 | - name: Lint 22 | run: bin/rails standard 23 | 24 | test: 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | - name: Set up Ruby 30 | uses: ruby/setup-ruby@v1 31 | with: 32 | bundler-cache: true 33 | 34 | - name: Run tests 35 | run: bin/rails test 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /doc/ 3 | /log/*.log 4 | /pkg/ 5 | /tmp/ 6 | /test/dummy/db/*.sqlite3 7 | /test/dummy/db/*.sqlite3-* 8 | /test/dummy/log/*.log 9 | /test/dummy/storage/ 10 | /test/dummy/tmp/ 11 | node_modules 12 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.1 2 | -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- 1 | ruby_version: 3.0.5 2 | ignore: 3 | - "templates/partials/db_optimizations_configuration.rb" 4 | - "templates/partials/pull_requests_config.rb" 5 | - "templates/partials/email_smtp.rb" 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | 4 | # More details are here: https://help.github.com/articles/about-codeowners/ 5 | 6 | # The '*' pattern is global owners. 7 | 8 | # Global rule: 9 | * @stevepolitodesign 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | We love contributions from everyone. 4 | By participating in this project, 5 | you agree to abide by the thoughtbot [code of conduct]. 6 | 7 | [code of conduct]: https://thoughtbot.com/open-source-code-of-conduct 8 | 9 | We expect everyone to follow the code of conduct 10 | anywhere in thoughtbot's project codebases, 11 | issue trackers, chatrooms, and mailing lists. 12 | 13 | ## Contributing Code 14 | 15 | Fork the repo. 16 | 17 | Run the setup script. 18 | 19 | ``` 20 | ./bin/setup 21 | ``` 22 | 23 | Make sure the tests pass: 24 | 25 | ``` 26 | bin/rails test 27 | ``` 28 | 29 | Make sure there are no linting violations: 30 | 31 | ``` 32 | bin/rails standard 33 | ``` 34 | 35 | Make your change, with new passing tests. 36 | 37 | Mention how your changes affect the project to other developers and users in the 38 | `NEWS.md` file. 39 | 40 | Push to your fork. Write a [good commit message][commit]. Submit a pull request. 41 | 42 | [commit]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 43 | 44 | Others will give constructive feedback. 45 | This is a time for discussion and improvements, 46 | and making the necessary changes will be required before we can 47 | merge the contribution. 48 | 49 | ## Testing Generators 50 | 51 | There is a smaller dummy application at `test/dummy`. This application is used 52 | as a mounting point for the engine, to make testing the engine extremely simple. 53 | 54 | There are a number of [assertions][] and [helpers][] that make testing 55 | generators easier. 56 | 57 | [assertions]: https://api.rubyonrails.org/classes/Rails/Generators/Testing/Assertions.html 58 | [helpers]: https://api.rubyonrails.org/classes/Rails/Generators/Testing/Behavior.html 59 | 60 | ## Publishing to RubyGems 61 | 62 | When the gem is ready to be shared as a formal release, it can be 63 | [published][published] to RubyGems. 64 | 65 | [published]: https://guides.rubyonrails.org/plugins.html#publishing-your-gem 66 | 67 | 1. Bump the version number in `Suspenders::VERSION` 68 | 2. Run `bundle exec rake build` 69 | 3. Run `bundle exec rake install` 70 | 4. Run `bundle exec rake release` 71 | -------------------------------------------------------------------------------- /GOALS.md: -------------------------------------------------------------------------------- 1 | # Goals 2 | 3 | Suspenders is an opinionated platform for adding functionality to a Rails app. 4 | It encodes thoughtbot's opinions and decisions in tooling and best practices. 5 | 6 | We are transitioning to a library of generators. All new functionality must be 7 | in the form of a well-tested, self-contained generator that fits within the 8 | Suspenders framework. 9 | 10 | ## History and transitions 11 | 12 | In the beginning suspenders was a git repo that we would clone then use as a 13 | template for starting a new Rails app. 14 | 15 | It then became a command-line tool that would generate a Rails app then modify 16 | the files in place. Over time, and with intention, most of the modifications 17 | were focused on modifying the Gemfile. 18 | 19 | This was identified as unscalable and we have been slowing been moving in a new 20 | direction: a library of generators. 21 | 22 | ## The future 23 | 24 | The problem with a script that adds a bunch of gem requirements to the Gemfile 25 | is: what if you don't want all those gems? Suspenders adds gem requirements and 26 | then configures each one. On a typical project we would run Suspenders then 27 | spend the rest of the day un-doing the parts we don't want. 28 | 29 | In 2015, [PR #511], we discussed what we want: a gem that is a library of 30 | generators that we can run on demand. The ideal future workflow goes like this: 31 | 32 | [PR #511]: https://github.com/thoughtbot/suspenders/pull/511 33 | 34 | 1. `rails new my-cool-project && cd my-cool-project` 35 | 2. `echo "gem 'suspenders', group: :development" >> Gemfile && bundle` 36 | 3. `rails g suspenders:ci` 37 | 4. Repeat with other generators as needed. 38 | 39 | In this future you would use `rails new` as normal and instead add Suspenders 40 | as a development gem. This gives you access to opinionated generators for 41 | adding (and removing!) whole functionality, but on demand and with your 42 | knowledge. 43 | 44 | The generators are all named to generically describe what functionality they 45 | add. In this way we can change our opinions on the implementation without 46 | needing to know new generator names. For example, `suspenders:ci` could 47 | configure the app for CircleCI now, but down the road we may decide to switch 48 | everything to Jenkins; we could update the implementation of the generator 49 | without changing its name. 50 | 51 | ## From here to there 52 | 53 | The transition looks like this: 54 | 55 | 1. Introduce a `suspenders:all` generator that runs Suspenders against an 56 | existing Rails app. 57 | 2. Change the Suspenders program to create a Rails app, add Suspenders to the 58 | Gemfile, and run `suspenders:all` inside of it. 59 | 3. Slowly split functionality out from the "all" generator and into smaller, 60 | well-tested, self-contained generators. The "all" generator invokes these 61 | generators. 62 | 4. Once that is completed -- once "all" _only_ calls other generators -- then 63 | remove the Suspenders program and only distribute the gem. 64 | 65 | We are on step (3), and it's a big one. Come help out! 66 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | # Specify your gem's dependencies in suspenders.gemspec. 5 | gemspec 6 | 7 | gem "puma" 8 | 9 | gem "sqlite3", ">= 2.1" 10 | 11 | # Start debugger with binding.b [https://github.com/ruby/debug] 12 | # gem "debug", ">= 1.0.0" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) thoughtbot, inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Suspenders 2 | 3 | [![CI](https://github.com/thoughtbot/suspenders/actions/workflows/main.yml/badge.svg)](https://github.com/thoughtbot/suspenders/actions/workflows/main.yml) 4 | 5 | Suspenders is a [Rails Engine][] containing generators for configuring Rails 6 | applications with these [features][]. 7 | 8 | It is used by thoughtbot to get a jump start on a new or existing app. Use 9 | Suspenders if you're in a rush to build something amazing; don't use it if you 10 | like missing deadlines. 11 | 12 | [Rails Engine]: https://guides.rubyonrails.org/engines.html 13 | [features]: ./FEATURES.md 14 | 15 | ![Suspenders boy](https://media.tumblr.com/1TEAMALpseh5xzf0Jt6bcwSMo1_400.png) 16 | 17 | ## Requirements 18 | 19 | - Rails `~> 8.0` 20 | - Ruby `>= 3.1` 21 | - Node `>= 20.0.0` 22 | 23 | ## Usage 24 | 25 | Suspenders can be used to create a new Rails application, or to enhance an 26 | existing Rails application. 27 | 28 | ### With New Rails Applications 29 | 30 | This approach uses an [application template][] to generate a new Rails 31 | application with Suspenders. 32 | 33 | We skip the [default test framework][] in favor of [RSpec][], and [prefer 34 | PostgreSQL][] as our database. 35 | 36 | We skip [RuboCop rules by default][] in favor of our [holistic linting rules][]. 37 | 38 | #### Use the latest suspenders release: 39 | 40 | ``` 41 | rails new app_name \ 42 | --skip-rubocop \ 43 | --skip-test \ 44 | -d=postgresql \ 45 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 46 | ``` 47 | 48 | #### OR use the current (possibly unreleased) `main` branch of suspenders: 49 | 50 | ``` 51 | rails new app_name \ 52 | --suspenders-main \ 53 | --skip-rubocop \ 54 | --skip-test \ 55 | -d=postgresql \ 56 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 57 | ``` 58 | 59 | Then run `bin/setup` within the newly generated application. 60 | 61 | Alternatively, if you're using our [dotfiles][], then you can just run `rails new 62 | app_name`, or create your own [railsrc][] file with the following configuration: 63 | 64 | ``` 65 | --skip-rubocop 66 | --skip-test 67 | --database=postgresql 68 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 69 | ``` 70 | 71 | [application template]: https://guides.rubyonrails.org/rails_application_templates.html 72 | [default test framework]: https://guides.rubyonrails.org/testing.html 73 | [RSpec]: http://rspec.info 74 | [prefer PostgreSQL]: https://github.com/thoughtbot/dotfiles/pull/728 75 | [dotfiles]: https://github.com/thoughtbot/dotfiles 76 | [railsrc]: https://github.com/rails/rails/blob/7f7f9df8641e35a076fe26bd097f6a1b22cb4e2d/railties/lib/rails/generators/rails/app/USAGE#L5C1-L7 77 | [RuboCop rules by default]: https://guides.rubyonrails.org/v7.2/7_2_release_notes.html#add-omakase-rubocop-rules-by-default 78 | [holistic linting rules]: https://github.com/thoughtbot/suspenders/blob/main/FEATURES.md#linting 79 | 80 | ### With Existing Rails Applications 81 | 82 | Suspenders can be used on an existing Rails application by adding it to the 83 | `:development` and `:test` group. 84 | 85 | ```ruby 86 | group :development, :test do 87 | gem "suspenders" 88 | end 89 | ``` 90 | 91 | Once installed, you can invoke the web installation generator, which will 92 | invoke all generators. 93 | 94 | ``` 95 | bin/rails g suspenders:install:web 96 | ``` 97 | 98 | Or, you can invoke generators individually. To see a list of available 99 | generators run: 100 | 101 | ``` 102 | bin/rails g | grep suspenders 103 | ``` 104 | 105 | To learn more about a generator, run: 106 | 107 | ``` 108 | bin/rails g suspenders:[generator_name] --help 109 | ``` 110 | 111 | ### Available Tasks 112 | 113 | Suspenders ships with several custom Rake tasks. 114 | 115 | ``` 116 | bin/rails suspenders:rake 117 | bin/rails suspenders:db:migrate 118 | bin/rails suspenders:cleanup:organize_gemfile 119 | ``` 120 | 121 | ## Contributing 122 | 123 | See the [CONTRIBUTING] document. 124 | Thank you, [contributors]! 125 | 126 | [CONTRIBUTING]: CONTRIBUTING.md 127 | [contributors]: https://github.com/thoughtbot/suspenders/graphs/contributors 128 | 129 | ## License 130 | 131 | Suspenders is Copyright (c) thoughtbot, inc. 132 | It is free software, and may be redistributed 133 | under the terms specified in the [LICENSE] file. 134 | 135 | [LICENSE]: /LICENSE 136 | 137 | 138 | ## About thoughtbot 139 | 140 | ![thoughtbot](https://thoughtbot.com/thoughtbot-logo-for-readmes.svg) 141 | 142 | This repo is maintained and funded by thoughtbot, inc. 143 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 144 | 145 | We love open source software! 146 | See [our other projects][community]. 147 | We are [available for hire][hire]. 148 | 149 | [community]: https://thoughtbot.com/community?utm_source=github 150 | [hire]: https://thoughtbot.com/hire-us?utm_source=github 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Update `NEWS.md` to reflect the changes since last release. 4 | 2. Update `lib/suspenders/version.rb` file accordingly. 5 | 3. Commit changes. There shouldn't be code changes, and thus CI doesn't need to 6 | run; you can add `[ci skip]` to the commit message. 7 | 4. Tag the release: `git tag vVERSION -a -s`. The tag message should contain the 8 | appropriate `NEWS.md` subsection. 9 | 5. Push changes: `git push --tags` 10 | 6. Build and publish to rubygems: 11 | ```sh 12 | gem build suspenders.gemspec 13 | gem push suspenders-*.gem 14 | ``` 15 | 7. Add a new GitHub release: 16 | https://github.com/thoughtbot/suspenders/releases/new?tag=vVERSION 17 | 8. Announce the new release, making sure to thank the contributors who helped 18 | shape this version! 19 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "bundler/gem_tasks" 3 | require "minitest/test_task" 4 | require "standard/rake" 5 | 6 | require File.expand_path("test/dummy/config/application", __dir__) 7 | 8 | Rails.application.load_tasks 9 | 10 | Minitest::TestTask.create(:test) do |t| 11 | t.libs << "test" 12 | t.libs << "lib" 13 | t.warning = false 14 | t.test_globs = ["test/**/*_test.rb"] 15 | end 16 | 17 | task default: %i[test standard] 18 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Supported Versions 5 | 6 | Only the the latest version of this project is supported at a given time. If 7 | you find a security issue with an older version, please try updating to the 8 | latest version first. 9 | 10 | If for some reason you can't update to the latest version, please let us know 11 | your reasons so that we can have a better understanding of your situation. 12 | 13 | ## Reporting a Vulnerability 14 | 15 | For security inquiries or vulnerability reports, visit 16 | . 17 | 18 | If you have any suggestions to improve this policy, visit . 19 | 20 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails gems 3 | # installed from the root of your application. 4 | 5 | ENGINE_ROOT = File.expand_path("..", __dir__) 6 | ENGINE_PATH = File.expand_path("../lib/suspenders/engine", __dir__) 7 | APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) 8 | 9 | # Set up gems listed in the Gemfile. 10 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 11 | require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) 12 | 13 | require "rails/all" 14 | require "rails/engine/commands" 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $: << File.expand_path("../test", __dir__) 3 | 4 | require "bundler/setup" 5 | require "rails/plugin/test" 6 | -------------------------------------------------------------------------------- /lib/generators/suspenders/accessibility_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class AccessibilityGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::APIAppUnsupported 5 | 6 | desc <<~MARKDOWN 7 | Uses [capybara_accessibility_audit][] and 8 | [capybara_accessible_selectors][] to encourage and enforce accessibility best 9 | practices. 10 | 11 | [capybara_accessibility_audit]: https://github.com/thoughtbot/capybara_accessibility_audit 12 | [capybara_accessible_selectors]: https://github.com/citizensadvice/capybara_accessible_selectors 13 | MARKDOWN 14 | 15 | def add_capybara_gems 16 | gem_group :test do 17 | gem "capybara_accessibility_audit", github: "thoughtbot/capybara_accessibility_audit" 18 | gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors", tag: "v0.12.0" 19 | end 20 | Bundler.with_unbundled_env { run "bundle install" } 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/suspenders/advisories_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class AdvisoriesGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates/advisories", __FILE__) 5 | desc <<~MARKDOWN 6 | Uses [bundler-audit][] to update the local security database and show 7 | any relevant issues with the app's dependencies via a Rake task. 8 | 9 | [bundler-audit]: https://github.com/rubysec/bundler-audit 10 | MARKDOWN 11 | 12 | def add_bundler_audit 13 | gem_group :development, :test do 14 | gem "bundler-audit", ">= 0.7.0", require: false 15 | end 16 | Bundler.with_unbundled_env { run "bundle install" } 17 | end 18 | 19 | def modify_rakefile 20 | content = <<~RUBY 21 | 22 | if Rails.env.local? 23 | require "bundler/audit/task" 24 | Bundler::Audit::Task.new 25 | end 26 | RUBY 27 | 28 | insert_into_file "Rakefile", content, after: /require_relative "config\/application"\n/ 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/generators/suspenders/ci_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class CiGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::DatabaseUnsupported 5 | include Suspenders::Generators::Helpers 6 | 7 | source_root File.expand_path("../../templates/ci", __FILE__) 8 | desc <<~MARKDOWN 9 | Uses [GitHub Actions][] for CI 10 | 11 | [GitHub Actions]: https://docs.github.com/en/actions 12 | MARKDOWN 13 | 14 | def ci_files 15 | template "ci.yml", ".github/workflows/ci.yml", force: true 16 | end 17 | 18 | private 19 | 20 | def scan_ruby? 21 | has_gem? "bundler-audit" 22 | end 23 | 24 | def scan_js? 25 | File.exist?("bin/importmap") && using_node? 26 | end 27 | 28 | def lint? 29 | using_node? && has_gem?("standard") && has_yarn_script?("lint") 30 | end 31 | 32 | def using_node? 33 | File.exist? "package.json" 34 | end 35 | 36 | def has_gem?(name) 37 | Bundler.rubygems.find_name(name).any? 38 | end 39 | 40 | def using_rspec? 41 | File.exist? "spec" 42 | end 43 | 44 | def has_yarn_script?(name) 45 | return false if !using_node? 46 | 47 | content = File.read("package.json") 48 | json = JSON.parse(content) 49 | 50 | json.dig("scripts", name) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/generators/suspenders/email_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class EmailGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates/email", __FILE__) 5 | desc <<~MARKDOWN 6 | [Intercept][] emails in non-production environments by setting `INTERCEPTOR_ADDRESSES`. 7 | 8 | ```sh 9 | INTERCEPTOR_ADDRESSES="user_1@example.com,user_2@example.com" bin/rails s 10 | ``` 11 | 12 | Configuration can be found at `config/initializers/email_interceptor.rb`. 13 | 14 | Interceptor can be found at `app/mailers/email_interceptor.rb`. 15 | 16 | [Intercept]: https://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails 17 | MARKDOWN 18 | 19 | def create_email_interceptor 20 | copy_file "email_interceptor.rb", "app/mailers/email_interceptor.rb" 21 | end 22 | 23 | def create_email_interceptor_initializer 24 | initializer "email_interceptor.rb", <<~RUBY 25 | Rails.application.configure do 26 | if ENV["INTERCEPTOR_ADDRESSES"].present? 27 | config.action_mailer.interceptors = %w[EmailInterceptor] 28 | end 29 | end 30 | RUBY 31 | end 32 | 33 | def configure_email_interceptor 34 | environment do 35 | %( 36 | config.to_prepare do 37 | EmailInterceptor.config.interceptor_addresses = ENV.fetch("INTERCEPTOR_ADDRESSES", "").split(",") 38 | end 39 | ) 40 | end 41 | end 42 | 43 | # TODO: Remove once https://github.com/rails/rails/pull/51191 is in latest 44 | # Rails release 45 | def configure_development_environment 46 | environment %(config.action_mailer.default_url_options = { host: "localhost", port: 3000 }), env: "development" 47 | end 48 | 49 | # TODO: Remove once https://github.com/rails/rails/pull/51191 is in latest 50 | # Rails release 51 | def configure_test_environment 52 | environment %(config.action_mailer.default_url_options = { host: "www.example.com" }), env: "test" 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/generators/suspenders/environments/development_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | module Environments 4 | class DevelopmentGenerator < Rails::Generators::Base 5 | desc <<~MARKDOWN 6 | - Enables [raise_on_missing_translations][]. 7 | - Enables [annotate_rendered_view_with_filenames][]. 8 | - Enables [i18n_customize_full_message][]. 9 | - Enables [query_log_tags_enabled][]. 10 | 11 | [raise_on_missing_translations]: https://guides.rubyonrails.org/configuring.html#config-i18n-raise-on-missing-translations 12 | [annotate_rendered_view_with_filenames]: https://guides.rubyonrails.org/configuring.html#config-action-view-annotate-rendered-view-with-filenames 13 | [i18n_customize_full_message]: https://guides.rubyonrails.org/configuring.html#config-active-model-i18n-customize-full-message 14 | [query_log_tags_enabled]: https://guides.rubyonrails.org/configuring.html#config-active-record-query-log-tags-enabled 15 | MARKDOWN 16 | 17 | def raise_on_missing_translations 18 | if development_config.match?(/^\s#\s*config\.i18n\.raise_on_missing_translations/) 19 | uncomment_lines "config/environments/development.rb", "config.i18n.raise_on_missing_translations = true" 20 | else 21 | environment %(config.i18n.raise_on_missing_translations = true), env: "development" 22 | end 23 | end 24 | 25 | def annotate_render_view_with_filename 26 | if development_config.match?(/^\s#\s*config\.action_view\.annotate_render_view_with_filename/) 27 | uncomment_lines "config/environments/development.rb", 28 | "config.action_view.annotate_rendered_view_with_filenames = true" 29 | else 30 | environment %(config.action_view.annotate_rendered_view_with_filenames = true), env: "development" 31 | end 32 | end 33 | 34 | def enable_i18n_customize_full_message 35 | environment %(config.active_model.i18n_customize_full_message = true), env: "development" 36 | end 37 | 38 | def enable_query_log_tags_enabled 39 | environment %(config.active_record.query_log_tags_enabled = true), env: "development" 40 | end 41 | 42 | private 43 | 44 | def development_config 45 | File.read(Rails.root.join("config/environments/development.rb")) 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/generators/suspenders/environments/production_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | module Environments 4 | class ProductionGenerator < Rails::Generators::Base 5 | desc <<~MARKDOWN 6 | - Enables [require_master_key][]. 7 | 8 | [require_master_key]: https://guides.rubyonrails.org/configuring.html#config-require-master-key 9 | MARKDOWN 10 | 11 | def require_master_key 12 | if production_config.match?(/^\s*#\s*config\.require_master_key\s*=\s*true/) 13 | uncomment_lines "config/environments/production.rb", /config\.require_master_key\s*=\s*true/ 14 | else 15 | environment %(config.require_master_key = true), env: "production" 16 | end 17 | end 18 | 19 | private 20 | 21 | def production_config 22 | File.read(Rails.root.join("config/environments/production.rb")) 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/generators/suspenders/environments/test_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | module Environments 4 | class TestGenerator < Rails::Generators::Base 5 | desc <<~MARKDOWN 6 | - Enables [raise_on_missing_translations][]. 7 | - Disables [action_dispatch.show_exceptions][]. 8 | 9 | [raise_on_missing_translations]: https://guides.rubyonrails.org/configuring.html#config-i18n-raise-on-missing-translations 10 | [action_dispatch.show_exceptions]: https://edgeguides.rubyonrails.org/configuring.html#config-action-dispatch-show-exceptions 11 | MARKDOWN 12 | 13 | def raise_on_missing_translations 14 | if test_config.match?(/^\s*#\s*config\.i18n\.raise_on_missing_translations\s*=\s*true/) 15 | uncomment_lines "config/environments/test.rb", /config\.i18n\.raise_on_missing_translations\s*=\s*true/ 16 | else 17 | environment %(config.i18n.raise_on_missing_translations = true), env: "test" 18 | end 19 | end 20 | 21 | def disable_action_dispatch_show_exceptions 22 | if test_config.match?(/^\s*config\.action_dispatch\.show_exceptions\s*=\s*:rescuable/) 23 | gsub_file "config/environments/test.rb", /^\s*config\.action_dispatch\.show_exceptions\s*=\s*:rescuable/, 24 | "config.action_dispatch.show_exceptions = :none" 25 | gsub_file "config/environments/test.rb", /^\s*#\s*Raise exceptions instead of rendering exception templates/i, "" 26 | else 27 | environment %(config.action_dispatch.show_exceptions = :none), env: "test" 28 | end 29 | end 30 | 31 | private 32 | 33 | def test_config 34 | File.read(Rails.root.join("config/environments/test.rb")) 35 | end 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/generators/suspenders/factories_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class FactoriesGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::Helpers 5 | 6 | source_root File.expand_path("../../templates/factories", __FILE__) 7 | desc <<~MARKDOWN 8 | Uses [FactoryBot][] as an alternative to [Fixtures][] to help you define 9 | dummy and test data for your test suite. The `create`, `build`, and 10 | `build_stubbed` class methods are directly available to all tests. 11 | 12 | Place FactoryBot definitions in `spec/factories.rb`, at least until it 13 | grows unwieldy. This helps reduce confusion around circular dependencies and 14 | makes it easy to jump between definitions. 15 | 16 | [FactoryBot]: https://github.com/thoughtbot/factory_bot 17 | [Fixtures]: https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures 18 | MARKDOWN 19 | 20 | def add_factory_bot 21 | gem_group :development, :test do 22 | gem "factory_bot_rails" 23 | end 24 | 25 | Bundler.with_unbundled_env { run "bundle install" } 26 | end 27 | 28 | def set_up_factory_bot 29 | if default_test_helper_present? 30 | insert_into_file Rails.root.join("test/test_helper.rb"), after: "class TestCase" do 31 | "\n include FactoryBot::Syntax::Methods" 32 | end 33 | elsif rspec_test_helper_present? 34 | copy_file "factory_bot_rspec.rb", "spec/support/factory_bot.rb" 35 | insert_into_file Rails.root.join("spec/rails_helper.rb") do 36 | %(Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file }) 37 | end 38 | end 39 | end 40 | 41 | def generate_empty_factories_file 42 | if default_test_suite? 43 | copy_file "factories.rb", "test/factories.rb" 44 | elsif rspec_test_suite? 45 | copy_file "factories.rb", "spec/factories.rb" 46 | end 47 | end 48 | 49 | def remove_fixture_definitions 50 | if default_test_helper_present? 51 | comment_lines "test/test_helper.rb", /fixtures :all/ 52 | end 53 | end 54 | 55 | def create_linting_test 56 | if default_test_suite? 57 | copy_file "factories_test.rb", "test/factory_bots/factories_test.rb" 58 | elsif rspec_test_suite? 59 | copy_file "factories_spec.rb", "spec/factory_bots/factories_spec.rb" 60 | end 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/generators/suspenders/inline_svg_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class InlineSvgGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::APIAppUnsupported 5 | source_root File.expand_path("../../templates/inline_svg", __FILE__) 6 | desc <<~MARKDOWN 7 | Uses [inline_svg][] for embedding SVG documents into views. 8 | 9 | Configuration can be found at `config/initializers/inline_svg.rb` 10 | 11 | [inline_svg]: https://github.com/jamesmartin/inline_svg 12 | MARKDOWN 13 | 14 | def add_inline_svg_gem 15 | gem "inline_svg" 16 | Bundler.with_unbundled_env { run "bundle install" } 17 | end 18 | 19 | def configure_inline_svg 20 | copy_file "inline_svg.rb", "config/initializers/inline_svg.rb" 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/generators/suspenders/install/web_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | module Install 4 | class WebGenerator < Rails::Generators::Base 5 | include Suspenders::Generators::APIAppUnsupported 6 | include Suspenders::Generators::DatabaseUnsupported 7 | include Suspenders::Generators::NodeNotInstalled 8 | include Suspenders::Generators::NodeVersionUnsupported 9 | 10 | source_root File.expand_path("../../../templates/install/web", __FILE__) 11 | desc <<~MARKDOWN 12 | Invokes all necessary generators for new Rails applications generated with Suspenders. 13 | 14 | This generatator is intended to be invoked as part of an [application template][]. 15 | 16 | #### Use the latest suspenders release: 17 | 18 | ``` 19 | rails new \\ 20 | --skip-rubocop \\ 21 | --skip-test \\ 22 | -d=postgresql \\ 23 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 24 | ``` 25 | 26 | #### OR use the current (possibly unreleased) `main` branch of suspenders: 27 | 28 | ``` 29 | rails new app_name \\ 30 | --suspenders-main \\ 31 | --skip-rubocop \\ 32 | --skip-test \\ 33 | -d=postgresql \\ 34 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 35 | ``` 36 | 37 | [application template]: https://guides.rubyonrails.org/rails_application_templates.html 38 | MARKDOWN 39 | 40 | def invoke_generators 41 | # This needs to go first, since it configures `.node-version` 42 | generate "suspenders:prerequisites" 43 | 44 | generate "suspenders:accessibility" 45 | generate "suspenders:advisories" 46 | generate "suspenders:email" 47 | generate "suspenders:factories" 48 | generate "suspenders:inline_svg" 49 | generate "suspenders:lint" 50 | generate "suspenders:rake" 51 | generate "suspenders:setup" 52 | generate "suspenders:tasks" 53 | generate "suspenders:testing" 54 | generate "suspenders:views" 55 | 56 | # suspenders:jobs needs to be invoked before suspenders:styles, since 57 | # suspenders:styles generator creates Procfile.dev 58 | generate "suspenders:styles" 59 | generate "suspenders:jobs" 60 | 61 | # Needs to run after other generators, since some touch the 62 | # configuration files. 63 | generate "suspenders:environments:test" 64 | generate "suspenders:environments:development" 65 | generate "suspenders:environments:production" 66 | 67 | # Needs to be run last since it depends on lint, testing, and 68 | # advisories 69 | generate "suspenders:ci" 70 | end 71 | 72 | def cleanup 73 | rake "suspenders:cleanup:organize_gemfile" 74 | rake "suspenders:cleanup:generate_readme" 75 | copy_file "CONTRIBUTING.md", "CONTRIBUTING.md" 76 | end 77 | 78 | def lint 79 | run "yarn run fix:prettier" 80 | run "bundle exec rake standard:fix_unsafely" 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/generators/suspenders/jobs_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class JobsGenerator < Rails::Generators::Base 4 | desc <<~MARKDOWN 5 | Uses [Sidekiq][] for [background job][] processing. 6 | 7 | Configures the `test` environment to use the [inline][] adapter. 8 | 9 | [Sidekiq]: https://github.com/sidekiq/sidekiq 10 | [background job]: https://guides.rubyonrails.org/active_job_basics.html 11 | [inline]: https://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html 12 | MARKDOWN 13 | 14 | def add_sidekiq_gem 15 | gem "sidekiq" 16 | Bundler.with_unbundled_env { run "bundle install" } 17 | end 18 | 19 | def configure_active_job 20 | environment "config.active_job.queue_adapter = :sidekiq" 21 | environment "config.active_job.queue_adapter = :inline", env: "test" 22 | end 23 | 24 | def configure_procfile 25 | if Rails.root.join("Procfile.dev").exist? 26 | append_to_file "Procfile.dev", "worker: bundle exec sidekiq" 27 | else 28 | say "Add default Procfile.dev" 29 | create_file "Procfile.dev", "worker: bundle exec sidekiq" 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/generators/suspenders/lint_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class LintGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::Helpers 5 | 6 | source_root File.expand_path("../../templates/lint", __FILE__) 7 | desc <<~MARKDOWN 8 | - Uses [@thoughtbot/eslint-config][] for JavaScript linting. 9 | - Uses [@thoughtbot/stylelint-config][] for CSS linting. 10 | - Uses [prettier][] for additional linting. 11 | - Uses [better_html][], [erb_lint][], and [erblint-github][] for ERB linting. 12 | - Uses [standard][] for Ruby linting. 13 | 14 | **Available Commands** 15 | 16 | - Run `yarn lint` to lint front-end code. 17 | - Run `yarn fix:prettier` to automatically fix prettier violations. 18 | - Run `bin/rails standard` to lint ERB and Ruby code. 19 | - Run `bundle exec standardrb --fix` to fix standard violations. 20 | 21 | [@thoughtbot/eslint-config]: https://github.com/thoughtbot/eslint-config 22 | [@thoughtbot/stylelint-config]: https://github.com/thoughtbot/stylelint-config 23 | [prettier]: https://prettier.io 24 | [better_html]: https://github.com/Shopify/better-html 25 | [erb_lint]: https://github.com/Shopify/erb-lint 26 | [erblint-github]: https://github.com/github/erblint-github 27 | [standard]: https://github.com/standardrb/standard 28 | MARKDOWN 29 | 30 | def check_package_json 31 | unless File.exist? Rails.root.join("package.json") 32 | copy_file "package.json", "package.json" 33 | end 34 | end 35 | 36 | # TODO: Remove eslint version pin once the follownig is solved 37 | # https://github.com/thoughtbot/eslint-config/issues/10 38 | def install_dependencies 39 | run "yarn add stylelint eslint@^8.9.0 @thoughtbot/stylelint-config @thoughtbot/eslint-config npm-run-all prettier --dev" 40 | end 41 | 42 | def install_gems 43 | gem_group :development, :test do 44 | gem "better_html", require: false 45 | gem "erb_lint", require: false 46 | gem "erblint-github", require: false 47 | gem "standard" 48 | end 49 | Bundler.with_unbundled_env { run "bundle install" } 50 | end 51 | 52 | def configure_stylelint 53 | copy_file "stylelintrc.json", ".stylelintrc.json" 54 | end 55 | 56 | def configure_eslint 57 | copy_file "eslintrc.json", ".eslintrc.json" 58 | end 59 | 60 | def configure_prettier 61 | copy_file "prettierrc", ".prettierrc" 62 | copy_file "prettierignore", ".prettierignore" 63 | end 64 | 65 | def configure_erb_lint 66 | copy_file "erb-lint.yml", ".erb-lint.yml" 67 | copy_file "config_better_html.yml", "config/better_html.yml" 68 | copy_file "config_initializers_better_html.rb", "config/initializers/better_html.rb" 69 | copy_file "erblint.rake", "lib/tasks/erblint.rake" 70 | template "rubocop.yml.tt", ".rubocop.yml" 71 | end 72 | 73 | def update_package_json 74 | content = File.read package_json 75 | json = JSON.parse content 76 | json["scripts"] ||= {} 77 | 78 | json["scripts"]["lint"] = "run-p lint:eslint lint:stylelint lint:prettier" 79 | json["scripts"]["lint:eslint"] = "eslint --max-warnings=0 --no-error-on-unmatched-pattern 'app/javascript/**/*.js'" 80 | json["scripts"]["lint:stylelint"] = "stylelint 'app/assets/stylesheets/**/*.css'" 81 | json["scripts"]["lint:prettier"] = "prettier --check '**/*' --ignore-unknown" 82 | json["scripts"]["fix:prettier"] = "prettier --write '**/*' --ignore-unknown" 83 | 84 | File.write package_json, JSON.pretty_generate(json) 85 | end 86 | 87 | private 88 | 89 | def package_json 90 | Rails.root.join("package.json") 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/generators/suspenders/prerequisites_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class PrerequisitesGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::Helpers 5 | include Suspenders::Generators::NodeNotInstalled 6 | include Suspenders::Generators::NodeVersionUnsupported 7 | 8 | source_root File.expand_path("../../templates/prerequisites", __FILE__) 9 | 10 | desc <<~MARKDOWN 11 | Creates `.node-version` file set to the current LTS version. 12 | MARKDOWN 13 | 14 | def set_node_version 15 | template "node-version", ".node-version" 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/suspenders/rake_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class RakeGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates/rake", __FILE__) 5 | desc <<~MARKDOWN 6 | Adds default Raketask wich is a wrapper for `suspenders:rake`. 7 | 8 | This will do the following: 9 | 10 | - Run the test suite. 11 | - Run a Ruby and ERB linter. 12 | - Scan the Ruby codebase for any dependency vulnerabilities. 13 | MARKDOWN 14 | 15 | def configure_default_rake_task 16 | append_to_file "Rakefile", <<~RUBY 17 | 18 | if Rails.env.local? 19 | task default: "suspenders:rake" 20 | end 21 | RUBY 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/generators/suspenders/setup_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class SetupGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates/setup", __FILE__) 5 | desc <<~MARKDOWN 6 | Run `bin/setup` to install dependencies and seed development data. 7 | MARKDOWN 8 | 9 | def replace_bin_setup 10 | copy_file "bin_setup.rb", "bin/setup", force: true 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/generators/suspenders/styles_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class StylesGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::APIAppUnsupported 5 | 6 | source_root File.expand_path("../../templates/styles", __FILE__) 7 | desc <<~MARKDOWN 8 | - Uses [PostCSS][] via [cssbundling-rails][]. 9 | - Uses [modern-normalize][] to normalize browsers' default style. 10 | 11 | Configuration can be found at `postcss.config.js`. 12 | 13 | Adds the following stylesheet structure. 14 | 15 | ``` 16 | app/assets/stylesheets/base.css 17 | app/assets/stylesheets/components.css 18 | app/assets/stylesheets/utilities.css 19 | ``` 20 | 21 | Adds `app/assets/static` so that [postcss-url][] has a directory to copy 22 | assets to. 23 | 24 | [PostCSS]: https://postcss.org 25 | [cssbundling-rails]: https://github.com/rails/cssbundling-rails 26 | [modern-normalize]: https://github.com/sindresorhus/modern-normalize 27 | [postcss-url]: https://github.com/postcss/postcss-url 28 | MARKDOWN 29 | 30 | def add_cssbundling_rails_gem 31 | gem "cssbundling-rails" 32 | 33 | Bundler.with_unbundled_env { run "bundle install" } 34 | run "bin/rails css:install:postcss" 35 | end 36 | 37 | def build_directory_structure 38 | create_file "app/assets/stylesheets/base.css" 39 | append_to_file "app/assets/stylesheets/base.css", "/* Base Styles */" 40 | 41 | create_file "app/assets/stylesheets/components.css" 42 | append_to_file "app/assets/stylesheets/components.css", "/* Component Styles */" 43 | 44 | create_file "app/assets/stylesheets/utilities.css" 45 | append_to_file "app/assets/stylesheets/utilities.css", "/* Utility Styles */" 46 | end 47 | 48 | def configure_application_stylesheet 49 | run "yarn add modern-normalize" 50 | 51 | append_to_file "app/assets/stylesheets/application.postcss.css" do 52 | <<~TEXT 53 | @import "modern-normalize"; 54 | @import "base.css"; 55 | @import "components.css"; 56 | @import "utilities.css"; 57 | TEXT 58 | end 59 | end 60 | 61 | def install_postcss_url 62 | run "yarn add postcss-url" 63 | end 64 | 65 | def configures_postcss 66 | begin 67 | File.delete(postcss_config) 68 | rescue Errno::ENOENT 69 | end 70 | 71 | empty_directory "app/assets/static" 72 | create_file "app/assets/static/.gitkeep" 73 | 74 | copy_file "postcss.config.js", "postcss.config.js" 75 | end 76 | 77 | private 78 | 79 | def postcss_config 80 | Rails.root.join("postcss.config.js") 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/generators/suspenders/tasks_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class TasksGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates/tasks", __FILE__) 5 | desc <<~MARKDOWN 6 | Creates `lib/tasks/dev.rake` which contains the following tasks: 7 | 8 | `bin/rails dev:prime` which loads sample data for local development. 9 | MARKDOWN 10 | 11 | def create_dev_rake 12 | if Bundler.rubygems.find_name("factory_bot").any? 13 | copy_file "dev.rake", "lib/tasks/dev.rake" 14 | else 15 | say "This generator requires Factory Bot" 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/generators/suspenders/testing_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class TestingGenerator < Rails::Generators::Base 4 | source_root File.expand_path("../../templates/testing", __FILE__) 5 | desc <<~MARKDOWN 6 | Uses [RSpec][] and [RSpec Rails][] in favor of the [default test suite][]. 7 | 8 | The test suite can be run with `bin/rails spec`. 9 | 10 | Configuration can be found in the following files: 11 | 12 | ``` 13 | spec/rails_helper.rb 14 | spec/spec_helper.rb 15 | spec/support/action_mailer.rb 16 | spec/support/driver.rb 17 | spec/support/i18n.rb 18 | spec/support/shoulda_matchers.rb 19 | ``` 20 | 21 | - Uses [action_dispatch-testing-integration-capybara][] to introduce Capybara assertions into Request specs. 22 | - Uses [shoulda-matchers][] for simple one-liner tests for common Rails functionality. 23 | - Uses [webmock][] for stubbing and setting expectations on HTTP requests in Ruby. 24 | 25 | [RSpec]: http://rspec.info 26 | [RSpec Rails]: https://github.com/rspec/rspec-rails 27 | [default test suite]: https://guides.rubyonrails.org/testing.html 28 | [action_dispatch-testing-integration-capybara]: https://github.com/thoughtbot/action_dispatch-testing-integration-capybara 29 | [shoulda-matchers]: https://github.com/thoughtbot/shoulda-matchers 30 | [webmock]: https://github.com/bblimke/webmock 31 | MARKDOWN 32 | 33 | def add_gems 34 | gem_group :development, :test do 35 | gem "rspec-rails", "~> 6.1.0" 36 | end 37 | 38 | gem_group :test do 39 | gem "capybara" 40 | gem "action_dispatch-testing-integration-capybara", 41 | github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", 42 | require: "action_dispatch/testing/integration/capybara/rspec" 43 | gem "selenium-webdriver" 44 | gem "shoulda-matchers", "~> 6.0" 45 | gem "webmock" 46 | end 47 | 48 | Bundler.with_unbundled_env { run "bundle install" } 49 | end 50 | 51 | def run_rspec_installation_script 52 | rails_command "generate rspec:install" 53 | end 54 | 55 | def modify_rails_helper 56 | insert_into_file "spec/rails_helper.rb", 57 | "\s\sconfig.infer_base_class_for_anonymous_controllers = false\n", 58 | after: "RSpec.configure do |config|\n" 59 | 60 | uncomment_lines "spec/rails_helper.rb", /Rails\.root\.glob/ 61 | end 62 | 63 | def modify_spec_helper 64 | persistence_file_path = "\s\sconfig.example_status_persistence_file_path = \"tmp/rspec_examples.txt\"\n" 65 | order = "\s\sconfig.order = :random\n\n" 66 | webmock_config = <<~RUBY 67 | 68 | WebMock.disable_net_connect!( 69 | allow_localhost: true, 70 | allow: [ 71 | /(chromedriver|storage).googleapis.com/, 72 | "googlechromelabs.github.io" 73 | ] 74 | ) 75 | RUBY 76 | 77 | insert_into_file "spec/spec_helper.rb", 78 | persistence_file_path + order, 79 | after: "RSpec.configure do |config|\n" 80 | 81 | insert_into_file "spec/spec_helper.rb", "require \"webmock/rspec\"\n\n", before: "RSpec.configure do |config|" 82 | insert_into_file "spec/spec_helper.rb", webmock_config 83 | end 84 | 85 | def create_system_spec_dir 86 | empty_directory "spec/system" 87 | create_file "spec/system/.gitkeep" 88 | end 89 | 90 | def configure_chromedriver 91 | copy_file "driver.rb", "spec/support/driver.rb" 92 | end 93 | 94 | def configure_i18n_helper 95 | copy_file "i18n.rb", "spec/support/i18n.rb" 96 | end 97 | 98 | def configure_shoulda_matchers 99 | copy_file "shoulda_matchers.rb", "spec/support/shoulda_matchers.rb" 100 | end 101 | 102 | def configure_action_mailer_helpers 103 | # https://guides.rubyonrails.org/testing.html#the-basic-test-case 104 | # 105 | # The ActionMailer::Base.deliveries array is only reset automatically in 106 | # ActionMailer::TestCase and ActionDispatch::IntegrationTest tests. If 107 | # you want to have a clean slate outside these test cases, you can reset 108 | # it manually with: ActionMailer::Base.deliveries.clear 109 | copy_file "action_mailer.rb", "spec/support/action_mailer.rb" 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /lib/generators/suspenders/views_generator.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Generators 3 | class ViewsGenerator < Rails::Generators::Base 4 | include Suspenders::Generators::APIAppUnsupported 5 | 6 | source_root File.expand_path("../../templates/views", __FILE__) 7 | desc <<~MARKDOWN 8 | - A [partial][] for [flash messages][] is located in `app/views/application/_flashes.html.erb`. 9 | - Sets [lang][] attribute on `` element to `en` via `I18n.local`. 10 | - Dynamically sets `` via the [title][] gem. 11 | - Disables Turbo's [Prefetch][] in an effort to reduce unnecessary network requests. 12 | 13 | [partial]: https://guides.rubyonrails.org/layouts_and_rendering.html#using-partials 14 | [flash messages]: https://guides.rubyonrails.org/action_controller_overview.html#the-flash 15 | [lang]: https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang 16 | [title]: https://github.com/calebhearth/title 17 | [Prefetch]: https://turbo.hotwired.dev/handbook/drive#prefetching-links-on-hover 18 | MARKDOWN 19 | 20 | def install_gems 21 | gem "title" 22 | 23 | Bundler.with_unbundled_env { run "bundle install" } 24 | end 25 | 26 | def create_views 27 | copy_file "flashes.html.erb", "app/views/application/_flashes.html.erb" 28 | end 29 | 30 | def update_application_layout 31 | insert_into_file "app/views/layouts/application.html.erb", " <%= render \"flashes\" -%>\n", after: "<body>\n" 32 | gsub_file "app/views/layouts/application.html.erb", /<html>/, "<html lang=\"<%= I18n.locale %>\">" 33 | gsub_file "app/views/layouts/application.html.erb", /<title>.*<\/title>/, "<title><%= title %>" 34 | insert_into_file "app/views/layouts/application.html.erb", " \n", after: "" 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/generators/templates/ci/ci.yml.tt: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | <%- if scan_ruby? -%> 10 | scan_ruby: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Ruby 18 | uses: ruby/setup-ruby@v1 19 | with: 20 | ruby-version: .ruby-version 21 | bundler-cache: true 22 | 23 | - name: Scan for security vulnerabilities in Ruby dependencies 24 | run: | 25 | bin/rails bundle:audit:update 26 | bin/rails bundle:audit 27 | <% end -%> 28 | 29 | <%- if scan_js? -%> 30 | scan_js: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | 37 | - name: Set up Ruby 38 | uses: ruby/setup-ruby@v1 39 | with: 40 | ruby-version: .ruby-version 41 | bundler-cache: true 42 | 43 | - name: Setup Node 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version-file: .node-version 47 | 48 | - name: Install modules 49 | run: yarn install 50 | 51 | - name: Scan for security vulnerabilities in JavaScript dependencies 52 | run: | 53 | bin/importmap audit 54 | yarn audit 55 | <% end -%> 56 | 57 | <%- if lint? -%> 58 | lint: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout code 62 | uses: actions/checkout@v4 63 | 64 | - name: Set up Ruby 65 | uses: ruby/setup-ruby@v1 66 | with: 67 | ruby-version: .ruby-version 68 | bundler-cache: true 69 | 70 | - name: Setup Node 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version-file: .node-version 74 | 75 | - name: Install modules 76 | run: yarn install 77 | 78 | - name: Lint Ruby code for consistent style 79 | run: bin/rails standard 80 | 81 | - name: Lint front-end code for consistent style 82 | run: yarn lint 83 | <% end -%> 84 | 85 | test: 86 | runs-on: ubuntu-latest 87 | 88 | services: 89 | postgres: 90 | image: postgres 91 | env: 92 | POSTGRES_USER: postgres 93 | POSTGRES_PASSWORD: postgres 94 | ports: 95 | - 5432:5432 96 | options: --health-cmd="pg_isready" --health-interval=10s --health-timeout=5s --health-retries=3 97 | 98 | # redis: 99 | # image: redis 100 | # ports: 101 | # - 6379:6379 102 | # options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 103 | 104 | steps: 105 | - name: Install packages 106 | run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libvips postgresql-client libpq-dev 107 | 108 | - name: Checkout code 109 | uses: actions/checkout@v4 110 | 111 | - name: Set up Ruby 112 | uses: ruby/setup-ruby@v1 113 | with: 114 | ruby-version: .ruby-version 115 | bundler-cache: true 116 | 117 | <%- if using_node? -%> 118 | - name: Setup Node 119 | uses: actions/setup-node@v4 120 | with: 121 | node-version-file: .node-version 122 | 123 | - name: Install modules 124 | run: yarn install 125 | <%- end -%> 126 | 127 | - name: Run tests 128 | env: 129 | RAILS_ENV: test 130 | DATABASE_URL: postgres://postgres:postgres@localhost:5432 131 | # REDIS_URL: redis://localhost:6379/0 132 | <%- if using_rspec? -%> 133 | run: bin/rails db:setup spec 134 | <%- else -%> 135 | run: bin/rails db:setup test test:system 136 | <%- end -%> 137 | 138 | - name: Keep screenshots from failed system tests 139 | uses: actions/upload-artifact@v4 140 | if: failure() 141 | with: 142 | name: screenshots 143 | <%- if using_rspec? -%> 144 | path: ${{ github.workspace }}/tmp/capybara 145 | <%- else -%> 146 | path: ${{ github.workspace }}/tmp/screenshots 147 | <%- end -%> 148 | if-no-files-found: ignore 149 | -------------------------------------------------------------------------------- /lib/generators/templates/email/email_interceptor.rb: -------------------------------------------------------------------------------- 1 | class EmailInterceptor 2 | include ActiveSupport::Configurable 3 | 4 | config_accessor :interceptor_addresses, default: [] 5 | 6 | def self.delivering_email(message) 7 | to = interceptor_addresses 8 | 9 | message.to = to if to.any? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/templates/factories/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | end 3 | -------------------------------------------------------------------------------- /lib/generators/templates/factories/factories_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Factories" do 4 | it "has valid factoties" do 5 | FactoryBot.lint traits: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/generators/templates/factories/factories_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FactoryBotsTest < ActiveSupport::TestCase 4 | class FactoryLintingTest < FactoryBotsTest 5 | test "linting of factories" do 6 | FactoryBot.lint traits: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/templates/factories/factory_bot_rspec.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.use_parent_strategy = true 2 | 3 | RSpec.configure do |config| 4 | config.include FactoryBot::Syntax::Methods 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/templates/inline_svg/inline_svg.rb: -------------------------------------------------------------------------------- 1 | InlineSvg.configure do |config| 2 | config.raise_on_file_not_found = true 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/templates/install/web/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Accessibility Tooling 4 | 5 | Building accessible applications is a complex, multi-faceted, and mission critical endeavor. While tooling cannot guarantee an accessible experience, it can help raise the bar for the baseline experience in important ways. 6 | 7 | In addition to the W3C's [WAI Overview][], [WAI Authoring Practices Guide][], and [WAI-ARIA Specification][], along with MDN's [Accessibility Documentation][], the [thoughtbot handbook][] includes some high-level technical guidance. 8 | 9 | [WAI Overview]: https://www.w3.org/WAI/standards-guidelines/aria/ 10 | [WAI Authoring Practices Guide]: https://www.w3.org/WAI/ARIA/apg/ 11 | [WAI-ARIA Specification]: https://www.w3.org/TR/wai-aria/ 12 | [Accessibility Documentation]: https://developer.mozilla.org/en-US/docs/Web/Accessibility 13 | [thoughtbot guides]: https://github.com/thoughtbot/guides/blob/main/accessibility/README.md#development 14 | 15 | ### Capybara and the System Test Suite 16 | 17 | [Capybara][] is responsible for driving the application's [System Test][] suite through Real Life browser sessions. 18 | 19 | Capybara drives the page and makes assertions about its state and content through utilities called [Selectors][]. They're a layer of abstraction that's a blend of HTML tag names and ARIA role semantics. For example, Capybara uses the `:link` selector whether it needs to locate and click on an `` element through a call to [click_link][] or make an assertion about the presence and content of an `` element through a call to [assert_link][]. 20 | 21 | Out of the box, Capybara provides a wide range of [built-in selectors][] that cover the majority of a System Test harness' needs. In addition to the out of the box selectors, this project also depends on the [capybara_accessible_selectors][] gem to expand Capybara's set of selectors. 22 | 23 | If you find yourself in a situation where you're reaching for CSS selectors or other means of resolving nodes, you might benefit from a built-in selector or one provided by `capybara_accessible_selectors`. Take this test block, for example: 24 | 25 | ```ruby 26 | #
27 | # Some fields 28 | # 29 | # 30 | # 31 | # 35 | #
36 | 37 | # BEFORE 38 | within ".some-css .selector-chain" do 39 | find(".my-button").click 40 | find(".my-text-field").set "Hello, world" 41 | end 42 | 43 | # AFTER 44 | within :fieldset, "Some field" do 45 | click_button "Click me" 46 | fill_in "Fill me in", with: "Hello, world" 47 | end 48 | ``` 49 | 50 | The [Testing RSpec][] section of the `thoughtbot/guides` provides some general guidance for automated Acceptance Testing. 51 | 52 | [Capybara]: https://rubydoc.info/github/teamcapybara/capybara/master/ 53 | [System Test]: https://guides.rubyonrails.org/testing.html#system-testing 54 | [Selectors]: https://rubydoc.info/github/teamcapybara/capybara/master#selectors 55 | [click_link]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Node/Actions:click_link 56 | [assert_link]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Minitest/Assertions:assert_link 57 | [built-in selectors]: https://rubydoc.info/github/teamcapybara/capybara/master/Capybara/Selector#built-in-selectors 58 | [capybara_accessible_selectors]: https://github.com/citizensadvice/capybara_accessible_selectors 59 | [Testing RSpec]: https://github.com/thoughtbot/guides/blob/main/testing-rspec/README.md#acceptance-tests 60 | 61 | ### `capybara_accessibility_audit` and `axe.js` 62 | 63 | This project depends on the [capybara_accessibility_audit][] gem to enhance the application's System Test suite to audit the browser's DOM for statically detectable accessibility violations. Under the hood, `capybara_accessibility_audit` utilizes [axe.js][] for auditing. 64 | 65 | The categories of violation that `capybara_accessibility_audit` can detect span a wide range from insufficient [color contrast][] to invalid or insufficient [landmark-based information hierarchy][landmark], to [unnamed form controls][] and beyond. 66 | 67 | Out of the box, `capybara_accessibility_audit` will extend Capybara to conduct an accessibility audit after a variety of actions like `visit` and `click_link`. That project can [be configured to suit this application's needs][capybara_accessibility_audit-configuration]. 68 | 69 | If you can integrate `capybara_accessibility_audit` from the project's inception, you can start with (and maintain!) accessibility violation bankruptcy. 70 | 71 | [capybara_accessibility_audit]: https://github.com/thoughtbot/capybara_accessibility_audit 72 | [capybara_accessibility_audit-configuration]: https://github.com/thoughtbot/capybara_accessibility_audit?tab=readme-ov-file#frequently-asked-questions 73 | [axe.js]: https://www.deque.com/axe/ 74 | [color contrast]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Perceivable/Color_contrast 75 | [landmark]: https://developer.mozilla.org/en-US/blog/aria-accessibility-html-landmark-roles/ 76 | [unnamed form controls]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Text_labels_and_names#form_elements_must_be_labeled 77 | 78 | ### VoiceOver on macos 79 | 80 | VoiceOver is a screen reader integrated directly into the macOS operating system. You can learn more from [Apple's Accessibility Support][]. 81 | 82 | VoiceOver and Safari are tightly integrated. If you're developing this 83 | application on a Mac, you should familiarize yourself with how to navigate in 84 | Safari using VoiceOver. You can learn more about how to use VoiceOver from the 85 | [Get started with VoiceOver on Mac][]. You might also enjoy [Screen Reader 86 | Basics: VoiceOver][] in the A11ycasts series of videos from the "Chrome for Developers" YouTube channel. 87 | 88 | [![Watch Screen Reader Basics: VoiceOver](https://img.youtube.com/vi/5R-6WvAihms/maxresdefault.jpg)](https://www.youtube.com/watch?v=5R-6WvAihms) 89 | 90 | After you've written a System Test or have completed a feature, it can be **extremely** valuable to navigate to that part of the application and try your best to recreate the experience based on keyboard navigation and screen reader announcements alone. 91 | 92 | [Apple's Accessibility Support]: https://support.apple.com/accessibility 93 | [Get started with VoiceOver on Mac]: https://support.apple.com/guide/voiceover/get-started-vo4be8816d70/10/mac/14.0 94 | [Screen Reader Basics: VoiceOver]: https://www.youtube.com/watch?v=5R-6WvAihms 95 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/config_better_html.yml: -------------------------------------------------------------------------------- 1 | --- 2 | allow_single_quoted_attributes: false 3 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/config_initializers_better_html.rb: -------------------------------------------------------------------------------- 1 | Rails.configuration.to_prepare do 2 | if Rails.env.test? 3 | require "better_html" 4 | 5 | BetterHtml.config = BetterHtml::Config.new(Rails.configuration.x.better_html) 6 | 7 | BetterHtml.config.template_exclusion_filter = proc { |filename| !filename.start_with?(Rails.root.to_s) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/erb-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | glob: "app/views/**/*.{html,turbo_stream}{+*,}.erb" 3 | 4 | linters: 5 | AllowedScriptType: 6 | enabled: true 7 | allowed_types: 8 | - "module" 9 | - "text/javascript" 10 | ErbSafety: 11 | enabled: true 12 | better_html_config: "config/better_html.yml" 13 | GitHub::Accessibility::AvoidBothDisabledAndAriaDisabledCounter: 14 | enabled: true 15 | GitHub::Accessibility::AvoidGenericLinkTextCounter: 16 | enabled: true 17 | GitHub::Accessibility::DisabledAttributeCounter: 18 | enabled: true 19 | GitHub::Accessibility::IframeHasTitleCounter: 20 | enabled: true 21 | GitHub::Accessibility::ImageHasAltCounter: 22 | enabled: true 23 | GitHub::Accessibility::LandmarkHasLabelCounter: 24 | enabled: true 25 | GitHub::Accessibility::LinkHasHrefCounter: 26 | enabled: true 27 | GitHub::Accessibility::NestedInteractiveElementsCounter: 28 | enabled: true 29 | GitHub::Accessibility::NoAriaLabelMisuseCounter: 30 | enabled: true 31 | GitHub::Accessibility::NoPositiveTabIndexCounter: 32 | enabled: true 33 | GitHub::Accessibility::NoRedundantImageAltCounter: 34 | enabled: true 35 | GitHub::Accessibility::NoTitleAttributeCounter: 36 | enabled: true 37 | GitHub::Accessibility::SvgHasAccessibleTextCounter: 38 | enabled: true 39 | Rubocop: 40 | enabled: true 41 | rubocop_config: 42 | inherit_from: 43 | - .rubocop.yml 44 | 45 | Lint/EmptyBlock: 46 | Enabled: false 47 | Layout/InitialIndentation: 48 | Enabled: false 49 | Layout/TrailingEmptyLines: 50 | Enabled: false 51 | Layout/TrailingWhitespace: 52 | Enabled: false 53 | Layout/LeadingEmptyLines: 54 | Enabled: false 55 | Style/FrozenStringLiteralComment: 56 | Enabled: false 57 | Style/MultilineTernaryOperator: 58 | Enabled: false 59 | Lint/UselessAssignment: 60 | Exclude: 61 | - "app/views/**/*" 62 | 63 | EnableDefaultLinters: true 64 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/erblint.rake: -------------------------------------------------------------------------------- 1 | module ERBLint 2 | module RakeSupport 3 | # Allow command line flags set in STANDARDOPTS (like MiniTest's TESTOPTS) 4 | def self.argvify 5 | if ENV["ERBLINTOPTS"] 6 | ENV["ERBLINTOPTS"].split(/\s+/) 7 | else 8 | [] 9 | end 10 | end 11 | 12 | # DELETE THIS FILE AFTER MERGE: 13 | # 14 | # * https://github.com/Shopify/better-html/pull/95 15 | # 16 | def self.backport! 17 | BetterHtml::TestHelper::SafeErb::AllowedScriptType::VALID_JAVASCRIPT_TAG_TYPES.push("module") 18 | end 19 | end 20 | end 21 | 22 | desc "Lint templates with erb_lint" 23 | task "erblint" do 24 | require "erb_lint/cli" 25 | require "erblint-github/linters" 26 | 27 | ERBLint::RakeSupport.backport! 28 | 29 | cli = ERBLint::CLI.new 30 | success = cli.run(ERBLint::RakeSupport.argvify + ["--lint-all", "--format=compact"]) 31 | fail unless success 32 | end 33 | 34 | desc "Lint and automatically fix templates with erb_lint" 35 | task "erblint:autocorrect" do 36 | require "erb_lint/cli" 37 | require "erblint-github/linters" 38 | 39 | ERBLint::RakeSupport.backport! 40 | 41 | cli = ERBLint::CLI.new 42 | success = cli.run(ERBLint::RakeSupport.argvify + ["--lint-all", "--autocorrect"]) 43 | fail unless success 44 | end 45 | 46 | task "standard" => "erblint" 47 | task "standard:fix" => "erblint:autocorrect" 48 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@thoughtbot/eslint-config/prettier"], 3 | "parserOptions": { 4 | "ecmaVersion": "latest", 5 | "sourceType": "module" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "private": "true" 4 | } 5 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/prettierignore: -------------------------------------------------------------------------------- 1 | vendor/bundle/** 2 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "overrides": [ 4 | { 5 | "files": ["**/*.css", "**/*.scss", "**/*.html"], 6 | "options": { 7 | "singleQuote": false 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/rubocop.yml.tt: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: <%= RUBY_VERSION %> 3 | 4 | require: standard 5 | 6 | inherit_gem: 7 | standard: config/base.yml 8 | -------------------------------------------------------------------------------- /lib/generators/templates/lint/stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@thoughtbot/stylelint-config" 3 | } 4 | -------------------------------------------------------------------------------- /lib/generators/templates/prerequisites/node-version.tt: -------------------------------------------------------------------------------- 1 | <%= node_version %> 2 | -------------------------------------------------------------------------------- /lib/generators/templates/setup/bin_setup.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | require "bundler" 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path("..", __dir__) 7 | 8 | def system!(*args) 9 | system(*args, exception: true) 10 | end 11 | 12 | def using_node? 13 | File.exist? "package.json" 14 | end 15 | 16 | FileUtils.chdir APP_ROOT do 17 | puts "== Installing dependencies ==" 18 | system! "gem install bundler --conservative" 19 | system("bundle check") || system!("bundle install") 20 | system("yarn install --check-files") if using_node? 21 | 22 | puts "\n== Preparing database and adding development seed data ==" 23 | if File.exist? "lib/tasks/dev.rake" 24 | system! "bin/rails dev:prime" 25 | else 26 | system! "bin/rails db:prepare" 27 | end 28 | 29 | if Bundler.rubygems.find_name("sprockets") 30 | puts "\n== Generating assets ==" 31 | system! "bin/rails assets:clobber assets:precompile" 32 | end 33 | 34 | puts "\n== Removing old logs and tempfiles ==" 35 | system! "bin/rails log:clear tmp:clear" 36 | 37 | puts "\n== Restarting application server ==" 38 | system! "bin/rails restart" 39 | end 40 | -------------------------------------------------------------------------------- /lib/generators/templates/styles/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-nesting'), 5 | require('autoprefixer'), 6 | require('postcss-url')({ 7 | url: 'copy', 8 | assetsPath: 'app/assets/static' 9 | }) 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /lib/generators/templates/tasks/dev.rake: -------------------------------------------------------------------------------- 1 | if Rails.env.development? || Rails.env.test? 2 | require "factory_bot" 3 | 4 | namespace :dev do 5 | desc "Sample data for local development environment" 6 | task prime: "db:setup" do 7 | include FactoryBot::Syntax::Methods 8 | 9 | # create(:user, email: "user@example.com", password: "password") 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/templates/testing/action_mailer.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each) do 3 | ActionMailer::Base.deliveries.clear 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/templates/testing/driver.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each, type: :system) do 3 | driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/generators/templates/testing/i18n.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include ActionView::Helpers::TranslationHelper 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/templates/testing/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | with.test_framework :rspec 4 | with.library :rails 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/templates/views/flashes.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash.any? %> 2 |
3 | <% flash.each do |type, message| -%> 4 |
<%= message %>
5 | <% end -%> 6 |
7 | <% end %> 8 | -------------------------------------------------------------------------------- /lib/install/web.rb: -------------------------------------------------------------------------------- 1 | def node_version 2 | ENV["NODE_VERSION"] || `node --version`[/\d+\.\d+\.\d+/] 3 | end 4 | 5 | def node_not_installed? 6 | !node_version.present? 7 | end 8 | 9 | def node_version_unsupported? 10 | node_version < "20.0.0" 11 | end 12 | 13 | def apply_template! 14 | if node_not_installed? || node_version_unsupported? 15 | message = <<~ERROR 16 | 17 | 18 | === Node version unsupported === 19 | 20 | Suspenders requires Node >= 20.0.0 21 | ERROR 22 | 23 | fail Rails::Generators::Error, message 24 | end 25 | if options[:database] == "postgresql" && options[:skip_test] && options[:skip_rubocop] 26 | after_bundle do 27 | gem_group :development, :test do 28 | if ARGV.include?("--suspenders-main") 29 | gem "suspenders", github: "thoughtbot/suspenders", branch: "main" 30 | else 31 | gem "suspenders" 32 | end 33 | end 34 | 35 | run "bundle install" 36 | 37 | generate "suspenders:install:web" 38 | rails_command "db:prepare" 39 | rails_command "db:migrate" 40 | 41 | say "\nCongratulations! You just pulled our suspenders." 42 | end 43 | else 44 | message = <<~ERROR 45 | 46 | 47 | === Please use the correct options === 48 | 49 | # Use the latest suspenders release: 50 | rails new \\ 51 | --skip-rubocop \\ 52 | --skip-test \\ 53 | -d=postgresql \\ 54 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 55 | 56 | # OR use the current (possibly unreleased) `main` branch of suspenders: 57 | rails new \\ 58 | --suspenders-main \\ 59 | --skip-rubocop \\ 60 | --skip-test \\ 61 | -d=postgresql \\ 62 | -m=https://raw.githubusercontent.com/thoughtbot/suspenders/main/lib/install/web.rb 63 | 64 | ERROR 65 | 66 | fail Rails::Generators::Error, message 67 | end 68 | end 69 | 70 | apply_template! 71 | -------------------------------------------------------------------------------- /lib/suspenders.rb: -------------------------------------------------------------------------------- 1 | require "suspenders/version" 2 | require "suspenders/engine" 3 | require "suspenders/railtie" 4 | require "suspenders/generators" 5 | require "suspenders/cleanup/organize_gemfile" 6 | require "suspenders/cleanup/generate_readme" 7 | 8 | module Suspenders 9 | # Your code goes here... 10 | end 11 | -------------------------------------------------------------------------------- /lib/suspenders/cleanup/generate_readme.rb: -------------------------------------------------------------------------------- 1 | require "rails/generators" 2 | require_relative "../../generators/suspenders/environments/development_generator" 3 | require_relative "../../generators/suspenders/environments/test_generator" 4 | require_relative "../../generators/suspenders/environments/production_generator" 5 | require_relative "../../generators/suspenders/accessibility_generator" 6 | require_relative "../../generators/suspenders/advisories_generator" 7 | require_relative "../../generators/suspenders/email_generator" 8 | require_relative "../../generators/suspenders/factories_generator" 9 | require_relative "../../generators/suspenders/inline_svg_generator" 10 | require_relative "../../generators/suspenders/jobs_generator" 11 | require_relative "../../generators/suspenders/lint_generator" 12 | require_relative "../../generators/suspenders/styles_generator" 13 | require_relative "../../generators/suspenders/testing_generator" 14 | require_relative "../../generators/suspenders/views_generator" 15 | 16 | module Suspenders 17 | module Cleanup 18 | class GenerateReadme 19 | include Suspenders::Generators::Helpers 20 | 21 | def self.perform(readme, app_name) 22 | new(readme, app_name).perform 23 | end 24 | 25 | attr_reader :readme, :app_name 26 | 27 | def initialize(readme, app_name) 28 | @readme = readme 29 | @app_name = app_name 30 | end 31 | 32 | def perform 33 | File.open(readme, "w+") do |file| 34 | @file = file 35 | 36 | heading app_name.titleize, level: 1 37 | 38 | prerequisites 39 | 40 | local_development 41 | 42 | heading "Configuration", level: 2 43 | 44 | heading "Test", level: 3 45 | description_for Suspenders::Generators::Environments::TestGenerator 46 | 47 | heading "Development", level: 3 48 | description_for Suspenders::Generators::Environments::DevelopmentGenerator 49 | 50 | heading "Production", level: 3 51 | description_for Suspenders::Generators::Environments::ProductionGenerator 52 | 53 | heading "Linting", level: 3 54 | description_for Suspenders::Generators::LintGenerator 55 | 56 | heading "Testing", level: 2 57 | description_for Suspenders::Generators::TestingGenerator 58 | 59 | heading "Factories", level: 3 60 | description_for Suspenders::Generators::FactoriesGenerator 61 | 62 | heading "Accessibility", level: 2 63 | description_for Suspenders::Generators::AccessibilityGenerator 64 | content <<~MARKDOWN 65 | For more information, review the [Accessibility Tooling][] section in 66 | the [CONTRIBUTING][] guide. 67 | 68 | [Accessibility Tooling]: ./CONTRIBUTING.md#accessibility-tooling 69 | [CONTRIBUTING]: ./CONTRIBUTING.md 70 | MARKDOWN 71 | new_line 72 | 73 | heading "Advisories", level: 2 74 | description_for Suspenders::Generators::AdvisoriesGenerator 75 | 76 | heading "Mailers", level: 2 77 | description_for Suspenders::Generators::EmailGenerator 78 | 79 | heading "Jobs", level: 2 80 | description_for Suspenders::Generators::JobsGenerator 81 | 82 | heading "Layout and Assets", level: 2 83 | 84 | heading "Stylesheets", level: 3 85 | description_for Suspenders::Generators::StylesGenerator 86 | 87 | heading "Inline SVG", level: 3 88 | description_for Suspenders::Generators::InlineSvgGenerator 89 | 90 | heading "Layout", level: 3 91 | description_for Suspenders::Generators::ViewsGenerator 92 | end 93 | end 94 | 95 | private 96 | 97 | def content(text) 98 | @file.write text 99 | end 100 | 101 | def new_line 102 | @file.write "\n" 103 | end 104 | 105 | def heading(text, level:) 106 | @file.write "#{"#" * level} #{text}\n" 107 | new_line 108 | end 109 | 110 | def description_for(generator) 111 | @file.write generator.desc 112 | new_line 113 | end 114 | 115 | def local_development 116 | @file.write <<~MARKDOWN 117 | ## Local Development 118 | 119 | ### Initial Setup 120 | 121 | ``` 122 | bin/setup 123 | ``` 124 | 125 | ### Running the Development Server 126 | 127 | ``` 128 | bin/dev 129 | ``` 130 | 131 | ### Seed Data 132 | 133 | - Use `db/seeds.rb` to create records that need to exist in all environments. 134 | - Use `lib/tasks/dev.rake` to create records that only need to exist in development. 135 | 136 | Running `bin/setup` will run `dev:prime`. 137 | 138 | ### Tasks 139 | 140 | - Use `bin/rails suspenders:db:migrate` to run [database migrations][]. This script ensures they are [reversible][]. 141 | - Use `bin/rails suspenders:cleanup:organize_gemfile` to automatically organize the project's Gemfile. 142 | - Use `bin/rails default` to run the default Rake task. This will do the following: 143 | - Run the test suite. 144 | - Run a Ruby and ERB linter. 145 | - Scan the Ruby codebase for any dependency vulnerabilities. 146 | 147 | [database migrations]: https://edgeguides.rubyonrails.org/active_record_migrations.html#running-migrations 148 | [reversible]: https://edgeguides.rubyonrails.org/active_record_migrations.html#making-the-irreversible-possible 149 | 150 | MARKDOWN 151 | end 152 | 153 | def prerequisites 154 | heading "Prerequisites", level: 2 155 | 156 | @file.write <<~MARKDOWN 157 | Ruby: `#{Suspenders::MINIMUM_RUBY_VERSION}` 158 | Node: `#{node_version}` 159 | MARKDOWN 160 | 161 | new_line 162 | end 163 | end 164 | end 165 | end 166 | -------------------------------------------------------------------------------- /lib/suspenders/cleanup/organize_gemfile.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | module Cleanup 3 | class OrganizeGemfile 4 | def self.perform(gemfile) 5 | new(gemfile).perform 6 | end 7 | 8 | attr_reader :gemfile, :current_lines, :new_lines, :new_line_markers, 9 | :current_group, :gem_groups 10 | 11 | def initialize(gemfile) 12 | @gemfile = gemfile 13 | 14 | @current_lines = File.read(gemfile).lines 15 | @new_lines = [] 16 | @new_line_markers = [] 17 | 18 | @current_group = nil 19 | @gem_groups = {} 20 | end 21 | 22 | def perform 23 | remove_line_breaks 24 | sort_gems_and_groups 25 | add_gem_groups_to_gemfile 26 | add_line_breaks 27 | cleanup 28 | 29 | File.open(gemfile, "w+") { _1.write new_lines.join } 30 | end 31 | 32 | private 33 | 34 | def remove_line_breaks 35 | current_lines.delete("\n") 36 | end 37 | 38 | def sort_gems_and_groups 39 | current_lines.each do |line| 40 | if line.starts_with?(/group/) 41 | @current_group = line 42 | end 43 | 44 | # Consolidate gem groups 45 | if current_group 46 | if line.starts_with?(/end/) 47 | @current_group = nil 48 | elsif !line.starts_with?(/group/) 49 | gem_groups[current_group] ||= [] 50 | gem_groups[current_group] << line 51 | end 52 | # Add non-grouped gems 53 | elsif !line.starts_with?(/\n/) 54 | new_lines << line 55 | @current_group = nil 56 | end 57 | end 58 | end 59 | 60 | def add_gem_groups_to_gemfile 61 | gem_groups.keys.each do |group| 62 | gems = gem_groups[group] 63 | 64 | gems.each_with_index do |gem, index| 65 | if index == 0 66 | new_lines << group 67 | end 68 | 69 | new_lines << gem 70 | 71 | if gems.size == (index + 1) 72 | new_lines << "end\n" 73 | end 74 | end 75 | end 76 | end 77 | 78 | def add_line_breaks 79 | new_lines.each_with_index do |line, index| 80 | previous_line = new_lines[index - 1] if index > 0 81 | next_line = new_lines[index + 1] 82 | marker = index + 1 83 | 84 | # Add line break if it's a gem and the next line is commented out 85 | if (line.starts_with?(/\s*gem/) || line.starts_with?(/\s*\#\s*gem/)) && next_line&.starts_with?(/\s*\#/) 86 | new_line_markers << marker 87 | end 88 | 89 | # Add line break if it's a commented out gem and the next line is a gem 90 | if line.starts_with?(/\s*\#\s*gem/) && next_line&.starts_with?(/\s*gem/) 91 | new_line_markers << marker 92 | end 93 | 94 | # Add line break if it's a gem with a comment and the next line is a gem 95 | if previous_line&.starts_with?(/\s*\#/) \ 96 | && line.starts_with?(/\s*gem/) \ 97 | && next_line&.starts_with?(/\s*gem/) \ 98 | && !previous_line.starts_with?(/\s*\#\s*gem/) 99 | new_line_markers << marker 100 | end 101 | 102 | # Add a line break if it's /end/ 103 | if line.starts_with?(/end/) 104 | new_line_markers << marker 105 | end 106 | 107 | # Add a line break if it's a gem and the next line is a group 108 | if line.starts_with?(/gem/) && next_line&.starts_with?(/group/) 109 | new_line_markers << marker 110 | end 111 | 112 | # Add line break if it's /source/ or /ruby/ 113 | if line.starts_with?(/\w/) && !line.starts_with?(/\s*(gem|group|end)/) 114 | new_line_markers << marker 115 | end 116 | end 117 | 118 | new_line_markers.each_with_index do |marker, index| 119 | # Each time we insert, the original marker if off by 1 120 | marker_offset = marker + index 121 | 122 | new_lines.insert(marker_offset, "\n") 123 | end 124 | end 125 | 126 | def cleanup 127 | # Remove last line 128 | if /\n/.match?(new_lines.last) 129 | new_lines.pop 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/suspenders/engine.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Suspenders 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/suspenders/generators.rb: -------------------------------------------------------------------------------- 1 | require "active_support/concern" 2 | 3 | module Suspenders 4 | module Generators 5 | module Helpers 6 | def default_test_suite? 7 | File.exist? Rails.root.join("test") 8 | end 9 | 10 | def rspec_test_suite? 11 | File.exist? Rails.root.join("spec/spec_helper.rb") 12 | end 13 | 14 | def default_test_helper_present? 15 | File.exist? Rails.root.join("test/test_helper.rb") 16 | end 17 | 18 | def rspec_test_helper_present? 19 | File.exist? Rails.root.join("spec/rails_helper.rb") 20 | end 21 | 22 | def node_version 23 | ENV["NODE_VERSION"] || `node --version`[/\d+\.\d+\.\d+/] 24 | end 25 | 26 | def node_not_installed? 27 | !node_version.present? 28 | end 29 | 30 | def node_version_unsupported? 31 | node_version < Suspenders::MINIMUM_NODE_VERSION 32 | end 33 | end 34 | 35 | module APIAppUnsupported 36 | class Error < StandardError 37 | def message 38 | "This generator cannot be used on API only applications." 39 | end 40 | end 41 | 42 | extend ActiveSupport::Concern 43 | 44 | included do 45 | def raise_if_api_only_app 46 | if api_only_app? 47 | raise Suspenders::Generators::APIAppUnsupported::Error 48 | end 49 | end 50 | end 51 | 52 | private 53 | 54 | def api_only_app? 55 | File.read(Rails.root.join("config/application.rb")) 56 | .match?(/^\s*config\.api_only\s*=\s*true/i) 57 | end 58 | end 59 | 60 | module DatabaseUnsupported 61 | include Helpers 62 | 63 | class Error < StandardError 64 | def message 65 | "This generator requires PostgreSQL" 66 | end 67 | end 68 | 69 | extend ActiveSupport::Concern 70 | 71 | included do 72 | def raise_if_database_unsupported 73 | if database_unsupported? 74 | raise Suspenders::Generators::DatabaseUnsupported::Error 75 | end 76 | end 77 | 78 | private 79 | 80 | def database_unsupported? 81 | configuration = File.read(Rails.root.join("config/database.yml")) 82 | configuration = YAML.safe_load(configuration, aliases: true) 83 | adapter = configuration["default"]["adapter"] 84 | 85 | adapter != "postgresql" 86 | end 87 | end 88 | end 89 | 90 | module NodeNotInstalled 91 | class Error < StandardError 92 | def message 93 | "This generator requires Node" 94 | end 95 | end 96 | 97 | extend ActiveSupport::Concern 98 | 99 | included do 100 | def raise_if_node_not_installed 101 | if node_not_installed? 102 | raise Suspenders::Generators::NodeNotInstalled::Error 103 | end 104 | end 105 | end 106 | end 107 | 108 | module NodeVersionUnsupported 109 | class Error < StandardError 110 | def message 111 | "This generator requires Node >= #{Suspenders::MINIMUM_NODE_VERSION}" 112 | end 113 | end 114 | 115 | extend ActiveSupport::Concern 116 | 117 | included do 118 | def raise_if_node_version_unsupported 119 | if node_version_unsupported? 120 | raise Suspenders::Generators::NodeVersionUnsupported::Error 121 | end 122 | end 123 | end 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/suspenders/railtie.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | class Railtie < ::Rails::Railtie 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/suspenders/version.rb: -------------------------------------------------------------------------------- 1 | module Suspenders 2 | VERSION = "20250317.0".freeze 3 | RAILS_VERSION = "~> 8.0".freeze 4 | MINIMUM_RUBY_VERSION = ">= 3.1".freeze 5 | MINIMUM_NODE_VERSION = "20.0.0".freeze 6 | end 7 | -------------------------------------------------------------------------------- /lib/tasks/suspenders.rake: -------------------------------------------------------------------------------- 1 | namespace :suspenders do 2 | desc "Extend the default Rails Rake task" 3 | task :rake do 4 | if Bundler.rubygems.find_name("bundler-audit").any? 5 | Rake::Task[:"bundle:audit"].invoke 6 | end 7 | 8 | if Bundler.rubygems.find_name("standard").any? 9 | Rake::Task[:standard].invoke 10 | end 11 | end 12 | 13 | desc "Ensure a migration is reversible" 14 | namespace :db do 15 | task :migrate do 16 | Rake::Task["db:migrate"].invoke 17 | Rake::Task["db:rollback"].invoke 18 | 19 | Rake::Task["db:migrate"].reenable 20 | Rake::Task["db:migrate"].invoke 21 | 22 | Rake::Task["db:test:prepare"].invoke 23 | end 24 | end 25 | 26 | namespace :cleanup do 27 | desc "Organizes Gemfile" 28 | task organize_gemfile: :environment do 29 | Suspenders::Cleanup::OrganizeGemfile.perform(Rails.root.join("Gemfile")) 30 | end 31 | 32 | desc "Generate README" 33 | task :generate_readme do 34 | Suspenders::Cleanup::GenerateReadme.perform(Rails.root.join("README.md"), Rails.application.class.module_parent_name) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /suspenders.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/suspenders/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "suspenders" 5 | spec.version = Suspenders::VERSION 6 | spec.required_ruby_version = Suspenders::MINIMUM_RUBY_VERSION 7 | spec.authors = ["thoughtbot"] 8 | spec.email = ["support@thoughtbot.com"] 9 | spec.homepage = "http://github.com/thoughtbot/suspenders" 10 | spec.summary = "Rails generators using thoughtbot's best practices." 11 | spec.description = <<~HERE 12 | Suspenders is a Rails plugin containing generators for configuring Rails 13 | applications. It is used by thoughtbot to get a jump start on a new or 14 | existing app. Use Suspenders if you're in a rush to build something amazing; 15 | don't use it if you like missing deadlines. 16 | HERE 17 | 18 | spec.license = "MIT" 19 | 20 | spec.metadata["homepage_uri"] = spec.homepage 21 | spec.metadata["source_code_uri"] = spec.homepage 22 | spec.metadata["changelog_uri"] = "https://github.com/thoughtbot/suspenders/blob/main/NEWS.md" 23 | 24 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 25 | Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] 26 | end 27 | 28 | spec.add_dependency "rails", Suspenders::RAILS_VERSION 29 | 30 | spec.add_development_dependency "climate_control" 31 | spec.add_development_dependency "mocha" 32 | spec.add_development_dependency "standard" 33 | end 34 | -------------------------------------------------------------------------------- /test/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /test/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/app/assets/images/.keep -------------------------------------------------------------------------------- /test/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* Application styles */ 2 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/channel.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Channel < ActionCable::Channel::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/channels/application_cable/connection.rb: -------------------------------------------------------------------------------- 1 | module ApplicationCable 2 | class Connection < ActionCable::Connection::Base 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /test/dummy/app/jobs/application_job.rb: -------------------------------------------------------------------------------- 1 | class ApplicationJob < ActiveJob::Base 2 | # Automatically retry jobs that encountered a deadlock 3 | # retry_on ActiveRecord::Deadlocked 4 | 5 | # Most jobs are safe to ignore if the underlying records are no longer available 6 | # discard_on ActiveJob::DeserializationError 7 | end 8 | -------------------------------------------------------------------------------- /test/dummy/app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: "from@example.com" 3 | layout "mailer" 4 | end 5 | -------------------------------------------------------------------------------- /test/dummy/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | primary_abstract_class 3 | end 4 | -------------------------------------------------------------------------------- /test/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/app/models/concerns/.keep -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | <%= stylesheet_link_tag "application" %> 10 | 11 | 12 | 13 | <%= yield %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/dummy/app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /test/dummy/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /test/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /test/dummy/bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /test/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | APP_ROOT = File.expand_path("..", __dir__) 5 | 6 | def system!(*args) 7 | system(*args, exception: true) 8 | end 9 | 10 | FileUtils.chdir APP_ROOT do 11 | # This script is a way to set up or update your development environment automatically. 12 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 13 | # Add necessary setup steps to this file. 14 | 15 | puts "== Installing dependencies ==" 16 | system("bundle check") || system!("bundle install") 17 | 18 | # puts "\n== Copying sample files ==" 19 | # unless File.exist?("config/database.yml") 20 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 21 | # end 22 | 23 | puts "\n== Preparing database ==" 24 | system! "bin/rails db:prepare" 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! "bin/rails log:clear tmp:clear" 28 | 29 | unless ARGV.include?("--skip-server") 30 | puts "\n== Starting development server ==" 31 | $stdout.flush # flush the output before exec(2) so that it displays 32 | exec "bin/dev" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/dummy/bin/thrust: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | load Gem.bin_path("thruster", "thrust") 6 | -------------------------------------------------------------------------------- /test/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative "boot" 2 | 3 | require "rails/all" 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Dummy 10 | class Application < Rails::Application 11 | config.load_defaults Rails::VERSION::STRING.to_f 12 | 13 | # Please, add to the `ignore` list any other `lib` subdirectories that do 14 | # not contain `.rb` files, or that should not be reloaded or eager loaded. 15 | # Common ones are `templates`, `generators`, or `middleware`, for example. 16 | config.autoload_lib(ignore: %w[assets tasks]) 17 | 18 | # Configuration for the application, engines, and railties goes here. 19 | # 20 | # These settings can be overridden in specific environments using the files 21 | # in config/environments, which are processed later. 22 | # 23 | # config.time_zone = "Central Time (US & Canada)" 24 | # config.eager_load_paths << Rails.root.join("extras") 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) 3 | 4 | require "bundler/setup" # Set up gems listed in the Gemfile. 5 | -------------------------------------------------------------------------------- /test/dummy/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: test 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: dummy_production 11 | -------------------------------------------------------------------------------- /test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite. Versions 3.8.0 and up are supported. 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem "sqlite3" 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: storage/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: storage/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: storage/production.sqlite3 26 | -------------------------------------------------------------------------------- /test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.application.configure do 4 | # Settings specified here will take precedence over those in config/application.rb. 5 | 6 | # Make code changes take effect immediately without server restart. 7 | config.enable_reloading = true 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable server timing. 16 | config.server_timing = true 17 | 18 | # Enable/disable Action Controller caching. By default Action Controller caching is disabled. 19 | # Run rails dev:cache to toggle Action Controller caching. 20 | if Rails.root.join("tmp/caching-dev.txt").exist? 21 | config.action_controller.perform_caching = true 22 | config.action_controller.enable_fragment_cache_logging = true 23 | config.public_file_server.headers = {"cache-control" => "public, max-age=#{2.days.to_i}"} 24 | else 25 | config.action_controller.perform_caching = false 26 | end 27 | 28 | # Change to :null_store to avoid any caching. 29 | config.cache_store = :memory_store 30 | 31 | # Store uploaded files on the local file system (see config/storage.yml for options). 32 | config.active_storage.service = :local 33 | 34 | # Don't care if the mailer can't send. 35 | config.action_mailer.raise_delivery_errors = false 36 | 37 | # Make template changes take effect immediately. 38 | config.action_mailer.perform_caching = false 39 | 40 | # Set localhost to be used by links generated in mailer templates. 41 | config.action_mailer.default_url_options = {host: "localhost", port: 3000} 42 | 43 | # Print deprecation notices to the Rails logger. 44 | config.active_support.deprecation = :log 45 | 46 | # Raise an error on page load if there are pending migrations. 47 | config.active_record.migration_error = :page_load 48 | 49 | # Highlight code that triggered database queries in logs. 50 | config.active_record.verbose_query_logs = true 51 | 52 | # Append comments with runtime information tags to SQL queries in logs. 53 | config.active_record.query_log_tags_enabled = true 54 | 55 | # Highlight code that enqueued background job in logs. 56 | config.active_job.verbose_enqueue_logs = true 57 | 58 | # Raises error for missing translations. 59 | # config.i18n.raise_on_missing_translations = true 60 | 61 | # Annotate rendered view with file names. 62 | config.action_view.annotate_rendered_view_with_filenames = true 63 | 64 | # Uncomment if you wish to allow Action Cable access from any origin. 65 | # config.action_cable.disable_request_forgery_protection = true 66 | 67 | # Raise error when a before_action's only/except options reference missing actions. 68 | config.action_controller.raise_on_missing_callback_actions = true 69 | 70 | # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. 71 | # config.generators.apply_rubocop_autocorrect_after_generate! 72 | end 73 | -------------------------------------------------------------------------------- /test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/integer/time" 2 | 3 | Rails.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.enable_reloading = false 8 | 9 | # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). 10 | config.eager_load = true 11 | 12 | # Full error reports are disabled. 13 | config.consider_all_requests_local = false 14 | 15 | # Turn on fragment caching in view templates. 16 | config.action_controller.perform_caching = true 17 | 18 | # Cache assets for far-future expiry since they are all digest stamped. 19 | config.public_file_server.headers = {"cache-control" => "public, max-age=#{1.year.to_i}"} 20 | 21 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 22 | # config.asset_host = "http://assets.example.com" 23 | 24 | # Store uploaded files on the local file system (see config/storage.yml for options). 25 | config.active_storage.service = :local 26 | 27 | # Assume all access to the app is happening through a SSL-terminating reverse proxy. 28 | config.assume_ssl = true 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | config.force_ssl = true 32 | 33 | # Skip http-to-https redirect for the default health check endpoint. 34 | # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } 35 | 36 | # Log to STDOUT with the current request id as a default log tag. 37 | config.log_tags = [:request_id] 38 | config.logger = ActiveSupport::TaggedLogging.logger($stdout) 39 | 40 | # Change to "debug" to log everything (including potentially personally-identifiable information!) 41 | config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") 42 | 43 | # Prevent health checks from clogging up the logs. 44 | config.silence_healthcheck_path = "/up" 45 | 46 | # Don't log any deprecations. 47 | config.active_support.report_deprecations = false 48 | 49 | # Replace the default in-process memory cache store with a durable alternative. 50 | # config.cache_store = :mem_cache_store 51 | 52 | # Replace the default in-process and non-durable queuing backend for Active Job. 53 | # config.active_job.queue_adapter = :resque 54 | 55 | # Ignore bad email addresses and do not raise email delivery errors. 56 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 57 | # config.action_mailer.raise_delivery_errors = false 58 | 59 | # Set host to be used by links generated in mailer templates. 60 | config.action_mailer.default_url_options = {host: "example.com"} 61 | 62 | # Specify outgoing SMTP server. Remember to add smtp/* credentials via rails credentials:edit. 63 | # config.action_mailer.smtp_settings = { 64 | # user_name: Rails.application.credentials.dig(:smtp, :user_name), 65 | # password: Rails.application.credentials.dig(:smtp, :password), 66 | # address: "smtp.example.com", 67 | # port: 587, 68 | # authentication: :plain 69 | # } 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Do not dump schema after migrations. 76 | config.active_record.dump_schema_after_migration = false 77 | 78 | # Only use :id for inspections in production. 79 | config.active_record.attributes_for_inspect = [:id] 80 | 81 | # Enable DNS rebinding protection and other `Host` header attacks. 82 | # config.hosts = [ 83 | # "example.com", # Allow requests from example.com 84 | # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` 85 | # ] 86 | # 87 | # Skip DNS rebinding protection for the default health check endpoint. 88 | # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } 89 | end 90 | -------------------------------------------------------------------------------- /test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | # The test environment is used exclusively to run your application's 2 | # test suite. You never need to work with it otherwise. Remember that 3 | # your test database is "scratch space" for the test suite and is wiped 4 | # and recreated between test runs. Don't rely on the data there! 5 | 6 | Rails.application.configure do 7 | # Settings specified here will take precedence over those in config/application.rb. 8 | 9 | # While tests run files are not watched, reloading is not necessary. 10 | config.enable_reloading = false 11 | 12 | # Eager loading loads your entire application. When running a single test locally, 13 | # this is usually not necessary, and can slow down your test suite. However, it's 14 | # recommended that you enable it in continuous integration systems to ensure eager 15 | # loading is working properly before deploying your code. 16 | config.eager_load = ENV["CI"].present? 17 | 18 | # Configure public file server for tests with cache-control for performance. 19 | config.public_file_server.headers = {"cache-control" => "public, max-age=3600"} 20 | 21 | # Show full error reports. 22 | config.consider_all_requests_local = true 23 | config.cache_store = :null_store 24 | 25 | # Render exception templates for rescuable exceptions and raise for other exceptions. 26 | config.action_dispatch.show_exceptions = :rescuable 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory. 32 | config.active_storage.service = :test 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Set host to be used by links generated in mailer templates. 40 | config.action_mailer.default_url_options = {host: "example.com"} 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations. 46 | # config.i18n.raise_on_missing_translations = true 47 | 48 | # Annotate rendered view with file names. 49 | # config.action_view.annotate_rendered_view_with_filenames = true 50 | 51 | # Raise error when a before_action's only/except options reference missing actions. 52 | config.action_controller.raise_on_missing_callback_actions = true 53 | end 54 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/content_security_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide content security policy. 4 | # See the Securing Rails Applications Guide for more information: 5 | # https://guides.rubyonrails.org/security.html#content-security-policy-header 6 | 7 | # Rails.application.configure do 8 | # config.content_security_policy do |policy| 9 | # policy.default_src :self, :https 10 | # policy.font_src :self, :https, :data 11 | # policy.img_src :self, :https, :data 12 | # policy.object_src :none 13 | # policy.script_src :self, :https 14 | # policy.style_src :self, :https 15 | # # Specify URI for violation reports 16 | # # policy.report_uri "/csp-violation-report-endpoint" 17 | # end 18 | # 19 | # # Generate session nonces for permitted importmap, inline scripts, and inline styles. 20 | # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } 21 | # config.content_security_policy_nonce_directives = %w(script-src style-src) 22 | # 23 | # # Report violations without enforcing the policy. 24 | # # config.content_security_policy_report_only = true 25 | # end 26 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += [ 7 | :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc 8 | ] 9 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/new_framework_defaults_8_0.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file eases your Rails 8.0 framework defaults upgrade. 4 | # 5 | # Uncomment each configuration one by one to switch to the new default. 6 | # Once your application is ready to run with all new defaults, you can remove 7 | # this file and set the `config.load_defaults` to `8.0`. 8 | # 9 | # Read the Guide for Upgrading Ruby on Rails for more info on each option. 10 | # https://guides.rubyonrails.org/upgrading_ruby_on_rails.html 11 | 12 | ### 13 | # Specifies whether `to_time` methods preserve the UTC offset of their receivers or preserves the timezone. 14 | # If set to `:zone`, `to_time` methods will use the timezone of their receivers. 15 | # If set to `:offset`, `to_time` methods will use the UTC offset. 16 | # If `false`, `to_time` methods will convert to the local system UTC offset instead. 17 | #++ 18 | # Rails.application.config.active_support.to_time_preserves_timezone = :zone 19 | 20 | ### 21 | # When both `If-Modified-Since` and `If-None-Match` are provided by the client 22 | # only consider `If-None-Match` as specified by RFC 7232 Section 6. 23 | # If set to `false` both conditions need to be satisfied. 24 | #++ 25 | # Rails.application.config.action_dispatch.strict_freshness = true 26 | 27 | ### 28 | # Set `Regexp.timeout` to `1`s by default to improve security over Regexp Denial-of-Service attacks. 29 | #++ 30 | # Regexp.timeout = 1 31 | -------------------------------------------------------------------------------- /test/dummy/config/initializers/permissions_policy.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Define an application-wide HTTP permissions policy. For further 4 | # information see: https://developers.google.com/web/updates/2018/06/feature-policy 5 | 6 | # Rails.application.config.permissions_policy do |policy| 7 | # policy.camera :none 8 | # policy.gyroscope :none 9 | # policy.microphone :none 10 | # policy.usb :none 11 | # policy.fullscreen :self 12 | # policy.payment :self, "https://secure.example.com" 13 | # end 14 | -------------------------------------------------------------------------------- /test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization and 2 | # are automatically loaded by Rails. If you want to use locales other than 3 | # 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 about the API, please read the Rails Internationalization guide 20 | # at https://guides.rubyonrails.org/i18n.html. 21 | # 22 | # Be aware that YAML interprets the following case-insensitive strings as 23 | # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings 24 | # must be quoted to be interpreted as strings. For example: 25 | # 26 | # en: 27 | # "yes": yup 28 | # enabled: "ON" 29 | 30 | en: 31 | hello: "Hello world" 32 | -------------------------------------------------------------------------------- /test/dummy/config/puma.rb: -------------------------------------------------------------------------------- 1 | # This configuration file will be evaluated by Puma. The top-level methods that 2 | # are invoked here are part of Puma's configuration DSL. For more information 3 | # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. 4 | # 5 | # Puma starts a configurable number of processes (workers) and each process 6 | # serves each request in a thread from an internal thread pool. 7 | # 8 | # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You 9 | # should only set this value when you want to run 2 or more workers. The 10 | # default is already 1. 11 | # 12 | # The ideal number of threads per worker depends both on how much time the 13 | # application spends waiting for IO operations and on how much you wish to 14 | # prioritize throughput over latency. 15 | # 16 | # As a rule of thumb, increasing the number of threads will increase how much 17 | # traffic a given process can handle (throughput), but due to CRuby's 18 | # Global VM Lock (GVL) it has diminishing returns and will degrade the 19 | # response time (latency) of the application. 20 | # 21 | # The default is set to 3 threads as it's deemed a decent compromise between 22 | # throughput and latency for the average Rails application. 23 | # 24 | # Any libraries that use a connection pool or another resource pool should 25 | # be configured to provide at least as many connections as the number of 26 | # threads. This includes Active Record's `pool` parameter in `database.yml`. 27 | threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) 28 | threads threads_count, threads_count 29 | 30 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 31 | port ENV.fetch("PORT", 3000) 32 | 33 | # Allow puma to be restarted by `bin/rails restart` command. 34 | plugin :tmp_restart 35 | 36 | # Run the Solid Queue supervisor inside of Puma for single-server deployments 37 | plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"] 38 | 39 | # Specify the PID file. Defaults to tmp/pids/server.pid in development. 40 | # In other environments, only set the PID file if requested. 41 | pidfile ENV["PIDFILE"] if ENV["PIDFILE"] 42 | -------------------------------------------------------------------------------- /test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html 3 | 4 | # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. 5 | # Can be used by load balancers and uptime monitors to verify that the app is live. 6 | get "up" => "rails/health#show", :as => :rails_health_check 7 | 8 | # Defines the root path route ("/") 9 | # root "posts#index" 10 | end 11 | -------------------------------------------------------------------------------- /test/dummy/config/storage.yml: -------------------------------------------------------------------------------- 1 | test: 2 | service: Disk 3 | root: <%= Rails.root.join("tmp/storage") %> 4 | 5 | local: 6 | service: Disk 7 | root: <%= Rails.root.join("storage") %> 8 | 9 | # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) 10 | # amazon: 11 | # service: S3 12 | # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> 13 | # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> 14 | # region: us-east-1 15 | # bucket: your_own_bucket-<%= Rails.env %> 16 | 17 | # Remember not to checkin your GCS keyfile to a repository 18 | # google: 19 | # service: GCS 20 | # project: your_project 21 | # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> 22 | # bucket: your_own_bucket-<%= Rails.env %> 23 | 24 | # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) 25 | # microsoft: 26 | # service: AzureStorage 27 | # storage_account_name: your_account_name 28 | # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> 29 | # container: your_container_name-<%= Rails.env %> 30 | 31 | # mirror: 32 | # service: Mirror 33 | # primary: local 34 | # mirrors: [ amazon, google, microsoft ] 35 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20241129142620_add_service_name_to_active_storage_blobs.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20190112182829) 2 | class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] 3 | def up 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | unless column_exists?(:active_storage_blobs, :service_name) 7 | add_column :active_storage_blobs, :service_name, :string 8 | 9 | if (configured_service = ActiveStorage::Blob.service.name) 10 | ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) 11 | end 12 | 13 | change_column :active_storage_blobs, :service_name, :string, null: false 14 | end 15 | end 16 | 17 | def down 18 | return unless table_exists?(:active_storage_blobs) 19 | 20 | remove_column :active_storage_blobs, :service_name 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20241129142621_create_active_storage_variant_records.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20191206030411) 2 | class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | # Use Active Record's configured type for primary key 7 | create_table :active_storage_variant_records, id: primary_key_type, if_not_exists: true do |t| 8 | t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type 9 | t.string :variation_digest, null: false 10 | 11 | t.index %i[blob_id variation_digest], name: "index_active_storage_variant_records_uniqueness", unique: true 12 | t.foreign_key :active_storage_blobs, column: :blob_id 13 | end 14 | end 15 | 16 | private 17 | 18 | def primary_key_type 19 | config = Rails.configuration.generators 20 | config.options[config.orm][:primary_key_type] || :primary_key 21 | end 22 | 23 | def blobs_primary_key_type 24 | pkey_name = connection.primary_key(:active_storage_blobs) 25 | pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } 26 | pkey_column.bigint? ? :bigint : pkey_column.type 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/dummy/db/migrate/20241129142622_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb: -------------------------------------------------------------------------------- 1 | # This migration comes from active_storage (originally 20211119233751) 2 | class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] 3 | def change 4 | return unless table_exists?(:active_storage_blobs) 5 | 6 | change_column_null(:active_storage_blobs, :checksum, true) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # This file is the source Rails uses to define your schema when running `bin/rails 6 | # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to 7 | # be faster and is potentially less error prone than running all of your 8 | # migrations from scratch. Old migrations may fail to apply correctly if those 9 | # migrations use external dependencies or application code. 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema[8.0].define(version: 2024_11_29_142622) do 14 | end 15 | -------------------------------------------------------------------------------- /test/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/lib/assets/.keep -------------------------------------------------------------------------------- /test/dummy/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/log/.keep -------------------------------------------------------------------------------- /test/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The page you were looking for doesn’t exist (404 Not found) 8 | 9 | 10 | 11 | 12 | 13 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |

The page you were looking for doesn’t exist. You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.

109 |
110 |
111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /test/dummy/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/public/apple-touch-icon.png -------------------------------------------------------------------------------- /test/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/public/favicon.ico -------------------------------------------------------------------------------- /test/dummy/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/public/icon.png -------------------------------------------------------------------------------- /test/dummy/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /test/dummy/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/storage/.keep -------------------------------------------------------------------------------- /test/dummy/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/tmp/.keep -------------------------------------------------------------------------------- /test/dummy/tmp/pids/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/tmp/pids/.keep -------------------------------------------------------------------------------- /test/dummy/tmp/storage/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thoughtbot/suspenders/2cf74f1890b837007f570dc4ad00996d96b2dbf6/test/dummy/tmp/storage/.keep -------------------------------------------------------------------------------- /test/fixtures/files/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative "config/application" 5 | 6 | Rails.application.load_tasks 7 | 8 | if Rails.env.local? 9 | task default: "suspenders:rake" 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/files/Rakefile_advisories: -------------------------------------------------------------------------------- 1 | require_relative "config/application" 2 | 3 | if Rails.env.local? 4 | require "bundler/audit/task" 5 | Bundler::Audit::Task.new 6 | end 7 | 8 | Rails.application.load_tasks 9 | -------------------------------------------------------------------------------- /test/fixtures/files/_flashes.html.erb: -------------------------------------------------------------------------------- 1 | <% if flash.any? %> 2 |
3 | <% flash.each do |type, message| -%> 4 |
<%= message %>
5 | <% end -%> 6 |
7 | <% end %> 8 | -------------------------------------------------------------------------------- /test/fixtures/files/action_mailer.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each) do 3 | ActionMailer::Base.deliveries.clear 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/files/better_html.rb: -------------------------------------------------------------------------------- 1 | Rails.configuration.to_prepare do 2 | if Rails.env.test? 3 | require "better_html" 4 | 5 | BetterHtml.config = BetterHtml::Config.new(Rails.configuration.x.better_html) 6 | 7 | BetterHtml.config.template_exclusion_filter = proc { |filename| !filename.start_with?(Rails.root.to_s) } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/files/driver.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.before(:each, type: :system) do 3 | driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/files/email_interceptor.rb: -------------------------------------------------------------------------------- 1 | class EmailInterceptor 2 | include ActiveSupport::Configurable 3 | 4 | config_accessor :interceptor_addresses, default: [] 5 | 6 | def self.delivering_email(message) 7 | to = interceptor_addresses 8 | 9 | message.to = to if to.any? 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/fixtures/files/email_interceptor_initializer.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | if ENV["INTERCEPTOR_ADDRESSES"].present? 3 | config.action_mailer.interceptors = %w[EmailInterceptor] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/files/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/files/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/files/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/files/erb-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | glob: "app/views/**/*.{html,turbo_stream}{+*,}.erb" 3 | 4 | linters: 5 | AllowedScriptType: 6 | enabled: true 7 | allowed_types: 8 | - "module" 9 | - "text/javascript" 10 | ErbSafety: 11 | enabled: true 12 | better_html_config: "config/better_html.yml" 13 | GitHub::Accessibility::AvoidBothDisabledAndAriaDisabledCounter: 14 | enabled: true 15 | GitHub::Accessibility::AvoidGenericLinkTextCounter: 16 | enabled: true 17 | GitHub::Accessibility::DisabledAttributeCounter: 18 | enabled: true 19 | GitHub::Accessibility::IframeHasTitleCounter: 20 | enabled: true 21 | GitHub::Accessibility::ImageHasAltCounter: 22 | enabled: true 23 | GitHub::Accessibility::LandmarkHasLabelCounter: 24 | enabled: true 25 | GitHub::Accessibility::LinkHasHrefCounter: 26 | enabled: true 27 | GitHub::Accessibility::NestedInteractiveElementsCounter: 28 | enabled: true 29 | GitHub::Accessibility::NoAriaLabelMisuseCounter: 30 | enabled: true 31 | GitHub::Accessibility::NoPositiveTabIndexCounter: 32 | enabled: true 33 | GitHub::Accessibility::NoRedundantImageAltCounter: 34 | enabled: true 35 | GitHub::Accessibility::NoTitleAttributeCounter: 36 | enabled: true 37 | GitHub::Accessibility::SvgHasAccessibleTextCounter: 38 | enabled: true 39 | Rubocop: 40 | enabled: true 41 | rubocop_config: 42 | inherit_from: 43 | - .rubocop.yml 44 | 45 | Lint/EmptyBlock: 46 | Enabled: false 47 | Layout/InitialIndentation: 48 | Enabled: false 49 | Layout/TrailingEmptyLines: 50 | Enabled: false 51 | Layout/TrailingWhitespace: 52 | Enabled: false 53 | Layout/LeadingEmptyLines: 54 | Enabled: false 55 | Style/FrozenStringLiteralComment: 56 | Enabled: false 57 | Style/MultilineTernaryOperator: 58 | Enabled: false 59 | Lint/UselessAssignment: 60 | Exclude: 61 | - "app/views/**/*" 62 | 63 | EnableDefaultLinters: true 64 | -------------------------------------------------------------------------------- /test/fixtures/files/eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@thoughtbot/eslint-config/prettier"], 3 | "parserOptions": { 4 | "ecmaVersion": "latest", 5 | "sourceType": "module" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/files/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryBot.define do 2 | end 3 | -------------------------------------------------------------------------------- /test/fixtures/files/factories_spec_lint.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe "Factories" do 4 | it "has valid factoties" do 5 | FactoryBot.lint traits: true 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/fixtures/files/factories_test_lint.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FactoryBotsTest < ActiveSupport::TestCase 4 | class FactoryLintingTest < FactoryBotsTest 5 | test "linting of factories" do 6 | FactoryBot.lint traits: true 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/files/gemfile_clean: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "3.3.0" 4 | 5 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 6 | gem "rails", "~> 7.1.3", ">= 7.1.3.2" 7 | 8 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 9 | gem "sprockets-rails" 10 | 11 | # Use postgresql as the database for Active Record 12 | gem "pg", "~> 1.1" 13 | 14 | # Use the Puma web server [https://github.com/puma/puma] 15 | gem "puma", ">= 5.0" 16 | 17 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 18 | gem "importmap-rails" 19 | 20 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 21 | gem "turbo-rails" 22 | 23 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 24 | gem "stimulus-rails" 25 | 26 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 27 | gem "jbuilder" 28 | 29 | # Use Redis adapter to run Action Cable in production 30 | gem "redis", ">= 4.0.1" 31 | 32 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 33 | # gem "kredis" 34 | 35 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 36 | # gem "bcrypt", "~> 3.1.7" 37 | 38 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 39 | gem "tzinfo-data", platforms: %i[windows jruby] 40 | 41 | # Reduces boot times through caching; required in config/boot.rb 42 | gem "bootsnap", require: false 43 | 44 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 45 | # gem "image_processing", "~> 1.2" 46 | 47 | gem "cssbundling-rails" 48 | gem "inline_svg" 49 | gem "sidekiq" 50 | gem "title" 51 | 52 | group :development, :test do 53 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 54 | gem "debug", platforms: %i[mri windows] 55 | 56 | gem "suspenders", github: "thoughtbot/suspenders", branch: "suspenders-3-0-0-web-generator" 57 | gem "bundler-audit", ">= 0.7.0", require: false 58 | gem "factory_bot_rails" 59 | gem "rspec-rails", "~> 6.1.0" 60 | gem "better_html", require: false 61 | gem "erb_lint", require: false 62 | gem "erblint-github", require: false 63 | gem "standard" 64 | end 65 | 66 | group :development do 67 | # Use console on exceptions pages [https://github.com/rails/web-console] 68 | gem "web-console" 69 | 70 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 71 | # gem "rack-mini-profiler" 72 | 73 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 74 | # gem "spring" 75 | end 76 | 77 | group :test do 78 | gem "capybara_accessibility_audit" 79 | gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors" 80 | gem "capybara" 81 | gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec" 82 | gem "selenium-webdriver" 83 | gem "shoulda-matchers", "~> 6.0" 84 | gem "webmock" 85 | end 86 | -------------------------------------------------------------------------------- /test/fixtures/files/gemfile_messy: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "3.3.0" 4 | 5 | # Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" 6 | gem "rails", "~> 7.1.3", ">= 7.1.3.2" 7 | 8 | # The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] 9 | gem "sprockets-rails" 10 | 11 | # Use postgresql as the database for Active Record 12 | gem "pg", "~> 1.1" 13 | 14 | # Use the Puma web server [https://github.com/puma/puma] 15 | gem "puma", ">= 5.0" 16 | 17 | # Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] 18 | gem "importmap-rails" 19 | 20 | # Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] 21 | gem "turbo-rails" 22 | 23 | # Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] 24 | gem "stimulus-rails" 25 | 26 | # Build JSON APIs with ease [https://github.com/rails/jbuilder] 27 | gem "jbuilder" 28 | 29 | # Use Redis adapter to run Action Cable in production 30 | gem "redis", ">= 4.0.1" 31 | 32 | # Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] 33 | # gem "kredis" 34 | 35 | # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] 36 | # gem "bcrypt", "~> 3.1.7" 37 | 38 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 39 | gem "tzinfo-data", platforms: %i[windows jruby] 40 | 41 | # Reduces boot times through caching; required in config/boot.rb 42 | gem "bootsnap", require: false 43 | 44 | # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] 45 | # gem "image_processing", "~> 1.2" 46 | 47 | group :development, :test do 48 | # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem 49 | gem "debug", platforms: %i[mri windows] 50 | end 51 | 52 | group :development do 53 | # Use console on exceptions pages [https://github.com/rails/web-console] 54 | gem "web-console" 55 | 56 | # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] 57 | # gem "rack-mini-profiler" 58 | 59 | # Speed up commands on slow machines / big apps [https://github.com/rails/spring] 60 | # gem "spring" 61 | end 62 | 63 | group :development, :test do 64 | gem "suspenders", github: "thoughtbot/suspenders", branch: "suspenders-3-0-0-web-generator" 65 | end 66 | 67 | group :test do 68 | gem "capybara_accessibility_audit" 69 | gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors" 70 | end 71 | gem "cssbundling-rails" 72 | 73 | group :development, :test do 74 | gem "bundler-audit", ">= 0.7.0", require: false 75 | end 76 | gem "inline_svg" 77 | 78 | group :development, :test do 79 | gem "factory_bot_rails" 80 | end 81 | gem "sidekiq" 82 | gem "title" 83 | 84 | group :development, :test do 85 | gem "rspec-rails", "~> 6.1.0" 86 | end 87 | 88 | group :test do 89 | gem "capybara" 90 | gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec" 91 | gem "selenium-webdriver" 92 | gem "shoulda-matchers", "~> 6.0" 93 | gem "webmock" 94 | end 95 | 96 | group :development, :test do 97 | gem "better_html", require: false 98 | gem "erb_lint", require: false 99 | gem "erblint-github", require: false 100 | gem "standard" 101 | end 102 | -------------------------------------------------------------------------------- /test/fixtures/files/i18n.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include ActionView::Helpers::TranslationHelper 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/files/inline_svg.rb: -------------------------------------------------------------------------------- 1 | InlineSvg.configure do |config| 2 | config.raise_on_file_not_found = true 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/files/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-nesting'), 5 | require('autoprefixer'), 6 | require('postcss-url')({ 7 | url: 'copy', 8 | assetsPath: 'app/assets/static' 9 | }) 10 | ], 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/files/prettierignore: -------------------------------------------------------------------------------- 1 | vendor/bundle/** 2 | -------------------------------------------------------------------------------- /test/fixtures/files/prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "overrides": [ 4 | { 5 | "files": ["**/*.css", "**/*.scss", "**/*.html"], 6 | "options": { 7 | "singleQuote": false 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/files/rails_helper.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | ENV["RAILS_ENV"] ||= "test" 3 | require_relative "../config/environment" 4 | 5 | abort("The Rails environment is running in production mode!") if Rails.env.production? 6 | require "rspec/rails" 7 | 8 | # Rails.root.glob("spec/support/**/*.rb").sort.each { |f| require f } 9 | 10 | begin 11 | ActiveRecord::Migration.maintain_test_schema! 12 | rescue ActiveRecord::PendingMigrationError => e 13 | abort e.to_s.strip 14 | end 15 | RSpec.configure do |config| 16 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 17 | config.use_transactional_fixtures = true 18 | config.infer_spec_type_from_file_location! 19 | config.filter_rails_from_backtrace! 20 | end 21 | Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file } 22 | -------------------------------------------------------------------------------- /test/fixtures/files/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | with.test_framework :rspec 4 | with.library :rails 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /test/fixtures/files/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "webmock/rspec" 2 | 3 | RSpec.configure do |config| 4 | config.example_status_persistence_file_path = "tmp/rspec_examples.txt" 5 | config.order = :random 6 | 7 | config.expect_with :rspec do |expectations| 8 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 9 | end 10 | 11 | config.mock_with :rspec do |mocks| 12 | mocks.verify_partial_doubles = true 13 | end 14 | config.shared_context_metadata_behavior = :apply_to_host_groups 15 | end 16 | 17 | WebMock.disable_net_connect!( 18 | allow_localhost: true, 19 | allow: [ 20 | /(chromedriver|storage).googleapis.com/, 21 | "googlechromelabs.github.io" 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /test/fixtures/files/stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@thoughtbot/stylelint-config" 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/files/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | require_relative "../config/environment" 3 | require "rails/test_help" 4 | 5 | module ActiveSupport 6 | class TestCase 7 | # Run tests in parallel with specified workers 8 | parallelize(workers: :number_of_processors) 9 | 10 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 11 | fixtures :all 12 | 13 | # Add more helper methods to be used by all tests here... 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/generators/suspenders/accessibility_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/accessibility_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class AccessibilityGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::AccessibilityGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "raises if API only application" do 15 | within_api_only_app do 16 | assert_raises Suspenders::Generators::APIAppUnsupported::Error do 17 | run_generator 18 | end 19 | 20 | assert_file app_root("Gemfile") do |file| 21 | assert_no_match "capybara_accessibility_audit", file 22 | assert_no_match "capybara_accessible_selectors", file 23 | end 24 | end 25 | end 26 | 27 | test "does not raise if API configuration is commented out" do 28 | within_api_only_app commented_out: true do 29 | run_generator 30 | 31 | assert_file app_root("Gemfile") do |file| 32 | assert_match "capybara_accessibility_audit", file 33 | assert_match "capybara_accessible_selectors", file 34 | end 35 | end 36 | end 37 | 38 | test "adds gems to Gemfile" do 39 | expected_output = <<~RUBY 40 | group :test do 41 | gem "capybara_accessibility_audit", github: "thoughtbot/capybara_accessibility_audit" 42 | gem "capybara_accessible_selectors", github: "citizensadvice/capybara_accessible_selectors", tag: "v0.12.0" 43 | end 44 | RUBY 45 | 46 | run_generator 47 | 48 | assert_file app_root("Gemfile") do |file| 49 | assert_match(expected_output, file) 50 | end 51 | end 52 | 53 | test "installs gems with Bundler" do 54 | output = run_generator 55 | 56 | assert_match(/bundle install/, output) 57 | end 58 | 59 | private 60 | 61 | def prepare_destination 62 | touch "Gemfile" 63 | end 64 | 65 | def restore_destination 66 | remove_file_if_exists "Gemfile" 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /test/generators/suspenders/advisories_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/advisories_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class AdvisoriesGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::AdvisoriesGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "adds gems to Gemfile" do 15 | expected_output = <<~RUBY 16 | group :development, :test do 17 | gem "bundler-audit", ">= 0.7.0", require: false 18 | end 19 | RUBY 20 | 21 | run_generator 22 | 23 | assert_file app_root("Gemfile") do |file| 24 | assert_match(expected_output, file) 25 | end 26 | end 27 | 28 | test "installs gems with Bundler" do 29 | output = run_generator 30 | 31 | assert_match(/bundle install/, output) 32 | end 33 | 34 | test "modifies Rakefile" do 35 | touch "Rakefile", content: <<~TEXT 36 | require_relative "config/application" 37 | 38 | Rails.application.load_tasks 39 | TEXT 40 | expected_rakefile = file_fixture("Rakefile_advisories").read 41 | 42 | run_generator 43 | 44 | assert_file app_root("Rakefile") do |file| 45 | assert_equal expected_rakefile, file 46 | end 47 | end 48 | 49 | private 50 | 51 | def prepare_destination 52 | touch "Gemfile" 53 | backup_file "Rakefile" 54 | end 55 | 56 | def restore_destination 57 | remove_file_if_exists "Gemfile" 58 | restore_file "Rakefile" 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/generators/suspenders/ci_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/ci_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class CiGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::CiGenerator 10 | destination Rails.root 11 | teardown :restore_destination 12 | 13 | test "generates CI files" do 14 | mkdir(".github/workflows") 15 | touch(".github/workflows/ci.yml") 16 | 17 | with_database "postgresql" do 18 | run_generator 19 | 20 | assert_file app_root(".github/workflows/ci.yml") 21 | end 22 | end 23 | 24 | test "raises if PostgreSQL is not the adapter" do 25 | with_database "unsupported" do 26 | assert_raises Suspenders::Generators::DatabaseUnsupported::Error, match: "This generator requires PostgreSQL" do 27 | run_generator 28 | 29 | assert_no_file app_root(".github/workflows/ci.yml") 30 | end 31 | end 32 | end 33 | 34 | private 35 | 36 | def restore_destination 37 | remove_dir_if_exists ".github" 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/generators/suspenders/email_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/email_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class EmailGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::EmailGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "creates a mailer intercepter" do 15 | expected = file_fixture("email_interceptor.rb").read 16 | 17 | run_generator 18 | 19 | assert_file app_root("app/mailers/email_interceptor.rb") do |file| 20 | assert_equal expected, file 21 | end 22 | end 23 | 24 | test "creates initializer" do 25 | expected = file_fixture("email_interceptor_initializer.rb").read 26 | 27 | run_generator 28 | 29 | assert_file app_root("config/initializers/email_interceptor.rb") do |file| 30 | assert_equal expected, file 31 | end 32 | end 33 | 34 | test "configures application to user email intercepter" do 35 | run_generator 36 | 37 | assert_file app_root("config/application.rb"), /config\.to_prepare\s*do.\s*EmailInterceptor\.config\.interceptor_addresses\s*=\s*ENV\.fetch\("INTERCEPTOR_ADDRESSES",\s*""\)\.split\(","\).\s*end/m 38 | end 39 | 40 | test "configures action mailer in development" do 41 | run_generator 42 | 43 | assert_file app_root("config/environments/development.rb"), /config\.action_mailer\.default_url_options\s*=\s*{\s*host:\s*"localhost",\s*port:\s*3000\s*}/ 44 | end 45 | 46 | test "configures action mailer in test" do 47 | run_generator 48 | 49 | assert_file app_root("config/environments/test.rb"), /config\.action_mailer\.default_url_options\s*=\s*{\s*host:\s*"www\.example\.com"\s*}/ 50 | end 51 | 52 | private 53 | 54 | def prepare_destination 55 | backup_file "config/application.rb" 56 | backup_file "config/environments/development.rb" 57 | backup_file "config/environments/test.rb" 58 | end 59 | 60 | def restore_destination 61 | restore_file "config/application.rb" 62 | restore_file "config/environments/development.rb" 63 | restore_file "config/environments/test.rb" 64 | remove_file_if_exists "app/mailers/email_interceptor.rb" 65 | remove_file_if_exists "config/initializers/email_interceptor.rb" 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/generators/suspenders/environments/development_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/environments/development_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | module Environments 7 | class DevelopmentGenerator::DefaultTest < Rails::Generators::TestCase 8 | include Suspenders::TestHelpers 9 | 10 | tests Suspenders::Generators::Environments::DevelopmentGenerator 11 | destination Rails.root 12 | setup :prepare_destination 13 | teardown :restore_destination 14 | 15 | test "raise on missing translations" do 16 | run_generator 17 | 18 | assert_file app_root("config/environments/development.rb") do |file| 19 | assert_match( 20 | /^ +config.i18n.raise_on_missing_translations = true$/, 21 | file 22 | ) 23 | end 24 | end 25 | 26 | test "raise on missing translations (when config is not commented out)" do 27 | content = file_fixture("environments/development.rb").read 28 | remove_file_if_exists "config/environments/development.rb" 29 | touch "config/environments/development.rb", content: content 30 | 31 | run_generator 32 | 33 | assert_file app_root("config/environments/development.rb") do |file| 34 | assert_match( 35 | /^ +config.i18n.raise_on_missing_translations = true$/, 36 | file 37 | ) 38 | end 39 | end 40 | 41 | test "annotate rendered view with file names" do 42 | run_generator 43 | 44 | assert_file app_root("config/environments/development.rb") do |file| 45 | assert_match( 46 | /^ +config.action_view.annotate_rendered_view_with_filenames = true$/, 47 | file 48 | ) 49 | end 50 | end 51 | 52 | test "annotate rendered view with file names (when config is not commented out)" do 53 | content = file_fixture("environments/development.rb").read 54 | remove_file_if_exists "config/environments/development.rb" 55 | touch "config/environments/development.rb", content: content 56 | 57 | run_generator 58 | 59 | assert_file app_root("config/environments/development.rb") do |file| 60 | assert_match( 61 | /^ +config.action_view.annotate_rendered_view_with_filenames = true$/, 62 | file 63 | ) 64 | end 65 | end 66 | 67 | test "enable active_model.i18n_customize_full_message" do 68 | run_generator 69 | 70 | assert_file app_root("config/environments/development.rb") do |file| 71 | assert_match(/^\s*config\.active_model\.i18n_customize_full_message\s*=\s*true/, file) 72 | end 73 | end 74 | 75 | test "enable active_record.query_log_tags_enabled" do 76 | run_generator 77 | 78 | assert_file app_root("config/environments/development.rb") do |file| 79 | assert_match(/^\s*config\.active_record\.query_log_tags_enabled\s*=\s*true/, file) 80 | end 81 | end 82 | 83 | private 84 | 85 | def prepare_destination 86 | backup_file "config/environments/development.rb" 87 | end 88 | 89 | def restore_destination 90 | restore_file "config/environments/development.rb" 91 | end 92 | end 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/generators/suspenders/environments/production_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/environments/production_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | module Environments 7 | class ProductionGeneratorTest < Rails::Generators::TestCase 8 | include Suspenders::TestHelpers 9 | 10 | tests Suspenders::Generators::Environments::ProductionGenerator 11 | destination Rails.root 12 | setup :prepare_destination 13 | teardown :restore_destination 14 | 15 | test "requires master key" do 16 | run_generator 17 | 18 | assert_file app_root("config/environments/production.rb") do |file| 19 | assert_match(/^\s*config\.require_master_key\s*=\s*true/, file) 20 | end 21 | end 22 | 23 | test "requires master key (when config is not commented out)" do 24 | content = file_fixture("environments/production.rb").read 25 | remove_file_if_exists "config/environments/production.rb" 26 | touch "config/environments/production.rb", content: content 27 | 28 | run_generator 29 | 30 | assert_file app_root("config/environments/production.rb") do |file| 31 | assert_match(/^\s*config\.require_master_key\s*=\s*true/, file) 32 | end 33 | end 34 | 35 | private 36 | 37 | def prepare_destination 38 | backup_file "config/environments/production.rb" 39 | end 40 | 41 | def restore_destination 42 | restore_file "config/environments/production.rb" 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/generators/suspenders/environments/test_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/environments/test_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | module Environments 7 | class TestGeneratorTest < Rails::Generators::TestCase 8 | include Suspenders::TestHelpers 9 | 10 | tests Suspenders::Generators::Environments::TestGenerator 11 | destination Rails.root 12 | setup :prepare_destination 13 | teardown :restore_destination 14 | 15 | test "raise on missing translations" do 16 | run_generator 17 | 18 | assert_file app_root("config/environments/test.rb") do |file| 19 | assert_match(/^\s*config\.i18n\.raise_on_missing_translations\s*=\s*true/, file) 20 | end 21 | end 22 | 23 | test "raise on missing translations (when config is not commented out)" do 24 | content = file_fixture("environments/test.rb").read 25 | remove_file_if_exists "config/environments/test.rb" 26 | touch "config/environments/test.rb", content: content 27 | 28 | run_generator 29 | 30 | assert_file app_root("config/environments/test.rb") do |file| 31 | assert_match(/^\s*config\.i18n\.raise_on_missing_translations\s*=\s*true/, file) 32 | end 33 | end 34 | 35 | test "disable action_dispatch.show_exceptions" do 36 | run_generator 37 | 38 | assert_file app_root("config/environments/test.rb") do |file| 39 | assert_match(/^\s*config\.action_dispatch\.show_exceptions\s*=\s*:none/, file) 40 | assert_no_match(/^\s*config\.action_dispatch\.show_exceptions\s*=\s*:rescuable/, file) 41 | assert_no_match(/^\s*#\s*Raise exceptions instead of rendering exception templates/i, file) 42 | end 43 | end 44 | 45 | test "disable action_dispatch.show_exceptions (when config does not exist)" do 46 | content = file_fixture("environments/test.rb").read 47 | remove_file_if_exists "config/environments/test.rb" 48 | touch "config/environments/test.rb", content: content 49 | 50 | run_generator 51 | 52 | assert_file app_root("config/environments/test.rb") do |file| 53 | assert_match(/^\s*config\.action_dispatch\.show_exceptions\s*=\s*:none/, file) 54 | end 55 | end 56 | 57 | private 58 | 59 | def prepare_destination 60 | backup_file "config/environments/test.rb" 61 | end 62 | 63 | def restore_destination 64 | restore_file "config/environments/test.rb" 65 | end 66 | end 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /test/generators/suspenders/factories_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/factories_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class FactoriesGenerator::DefaultTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::FactoriesGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "installs gem with Bundler" do 15 | with_test_suite :minitest do 16 | output = run_generator 17 | 18 | assert_match(/bundle install/, output) 19 | end 20 | end 21 | 22 | test "removes fixture definitions" do 23 | with_test_suite :minitest do 24 | run_generator 25 | 26 | assert_file app_root("test/test_helper.rb") do |file| 27 | assert_match(/# fixtures :all/, file) 28 | end 29 | end 30 | end 31 | 32 | test "adds gem to Gemfile" do 33 | with_test_suite :minitest do 34 | run_generator 35 | 36 | assert_file app_root("Gemfile") do |file| 37 | assert_match(/group :development, :test do\n gem "factory_bot_rails"\nend/, file) 38 | end 39 | end 40 | end 41 | 42 | test "includes syntax methods" do 43 | with_test_suite :minitest do 44 | run_generator 45 | 46 | assert_file app_root("test/test_helper.rb") do |file| 47 | assert_match(/class TestCase\n include FactoryBot::Syntax::Methods/, file) 48 | end 49 | end 50 | end 51 | 52 | test "creates definition file" do 53 | with_test_suite :minitest do 54 | definition_file = file_fixture("factories.rb").read 55 | 56 | run_generator 57 | 58 | assert_file app_root("test/factories.rb") do |file| 59 | assert_match definition_file, file 60 | end 61 | end 62 | end 63 | 64 | test "creates linting test" do 65 | with_test_suite :minitest do 66 | factories_test = file_fixture("factories_test_lint.rb").read 67 | 68 | run_generator 69 | 70 | assert_file app_root("test/factory_bots/factories_test.rb") do |file| 71 | assert_match factories_test, file 72 | end 73 | end 74 | end 75 | 76 | private 77 | 78 | def prepare_destination 79 | touch "Gemfile" 80 | end 81 | 82 | def restore_destination 83 | remove_file_if_exists "Gemfile" 84 | remove_dir_if_exists "lib/tasks" 85 | end 86 | 87 | def test_helper 88 | file_fixture("test_helper.rb").read 89 | end 90 | end 91 | 92 | class FactoriesGenerator::RSpecTest < Rails::Generators::TestCase 93 | include Suspenders::TestHelpers 94 | 95 | tests Suspenders::Generators::FactoriesGenerator 96 | destination Rails.root 97 | setup :prepare_destination 98 | teardown :restore_destination 99 | 100 | test "includes syntax methods" do 101 | with_test_suite :rspec do 102 | touch("spec/rails_helper.rb") 103 | factory_bot_config = <<~RUBY 104 | FactoryBot.use_parent_strategy = true 105 | 106 | RSpec.configure do |config| 107 | config.include FactoryBot::Syntax::Methods 108 | end 109 | RUBY 110 | 111 | run_generator 112 | 113 | assert_file app_root("spec/support/factory_bot.rb") do |file| 114 | assert_match factory_bot_config, file 115 | end 116 | assert_file app_root("spec/rails_helper.rb") do |file| 117 | assert_match(/Dir\[Rails\.root\.join\("spec\/support\/\*\*\/\*\.rb"\)\]\.sort\.each { \|file\| require file }/, file) 118 | end 119 | end 120 | end 121 | 122 | test "creates definition file" do 123 | with_test_suite :rspec do 124 | definition_file = file_fixture("factories.rb").read 125 | 126 | run_generator 127 | 128 | assert_file app_root("spec/factories.rb") do |file| 129 | assert_match definition_file, file 130 | end 131 | end 132 | end 133 | 134 | test "does not modify rails_helper if it's configured to include support files" do 135 | with_test_suite :rspec do 136 | rails_helper = <<~RUBY 137 | Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |file| require file } 138 | RUBY 139 | touch "spec/rails_helper.rb", content: rails_helper 140 | 141 | run_generator 142 | 143 | assert_file app_root("spec/rails_helper.rb") do |file| 144 | assert_equal rails_helper, file 145 | end 146 | end 147 | end 148 | 149 | test "creates linting test" do 150 | with_test_suite :rspec do 151 | factories_spec = file_fixture("factories_spec_lint.rb").read 152 | 153 | run_generator 154 | 155 | assert_file app_root("spec/factory_bots/factories_spec.rb") do |file| 156 | assert_match factories_spec, file 157 | end 158 | end 159 | end 160 | 161 | private 162 | 163 | def prepare_destination 164 | touch "Gemfile" 165 | end 166 | 167 | def restore_destination 168 | remove_file_if_exists "Gemfile" 169 | remove_dir_if_exists "lib/tasks" 170 | end 171 | end 172 | end 173 | end 174 | -------------------------------------------------------------------------------- /test/generators/suspenders/inline_svg_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/inline_svg_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class InlinveSvgGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::InlineSvgGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "raises if API only application" do 15 | within_api_only_app do 16 | assert_raises Suspenders::Generators::APIAppUnsupported::Error do 17 | run_generator 18 | end 19 | 20 | assert_file app_root("Gemfile") do |file| 21 | assert_no_match "inline_svg", file 22 | end 23 | end 24 | end 25 | 26 | test "does not raise if API configuration is commented out" do 27 | within_api_only_app commented_out: true do 28 | run_generator 29 | 30 | assert_file app_root("Gemfile") do |file| 31 | assert_match "inline_svg", file 32 | end 33 | end 34 | end 35 | 36 | test "adds gems to Gemfile" do 37 | expected_output = <<~RUBY 38 | gem "inline_svg" 39 | RUBY 40 | 41 | run_generator 42 | 43 | assert_file app_root("Gemfile") do |file| 44 | assert_match(expected_output, file) 45 | end 46 | end 47 | 48 | test "installs gems with Bundler" do 49 | output = run_generator 50 | 51 | assert_match(/bundle install/, output) 52 | end 53 | 54 | test "configures raising an error when an SVG file is not found" do 55 | expected_configuration = file_fixture("inline_svg.rb").read 56 | 57 | run_generator 58 | 59 | assert_file app_root("config/initializers/inline_svg.rb") do |file| 60 | assert_match(expected_configuration, file) 61 | end 62 | end 63 | 64 | private 65 | 66 | def prepare_destination 67 | touch "Gemfile" 68 | end 69 | 70 | def restore_destination 71 | remove_file_if_exists "Gemfile" 72 | remove_file_if_exists "config/initializers/inline_svg.rb" 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/generators/suspenders/install/web_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/install/web_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | module Install 7 | class WebGeneratorTest < Rails::Generators::TestCase 8 | include Suspenders::TestHelpers 9 | 10 | tests Suspenders::Generators::Install::WebGenerator 11 | destination Rails.root 12 | setup :prepare_destination 13 | teardown :restore_destination 14 | 15 | test "raises if API only application" do 16 | within_api_only_app do 17 | assert_raises Suspenders::Generators::APIAppUnsupported::Error do 18 | run_generator 19 | end 20 | end 21 | end 22 | 23 | test "raises if PostgreSQL is not the adapter" do 24 | with_database "unsupported" do 25 | assert_raises Suspenders::Generators::DatabaseUnsupported::Error do 26 | run_generator 27 | end 28 | end 29 | end 30 | 31 | test "raises if Node is not installed" do 32 | Object.any_instance.stubs(:`).returns("") 33 | 34 | with_database "postgresql" do 35 | assert_raises Suspenders::Generators::NodeNotInstalled::Error do 36 | run_generator 37 | end 38 | end 39 | end 40 | 41 | test "raises if Node is unsupported" do 42 | Object.any_instance.stubs(:`).returns("v19.9.9\n") 43 | 44 | with_database "postgresql" do 45 | assert_raises Suspenders::Generators::NodeVersionUnsupported::Error do 46 | run_generator 47 | end 48 | end 49 | end 50 | 51 | private 52 | 53 | def prepare_destination 54 | touch "Gemfile" 55 | end 56 | 57 | def restore_destination 58 | remove_file_if_exists "Gemfile" 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/generators/suspenders/jobs_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/jobs_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class JobsGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::JobsGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "adds gems to Gemfile" do 15 | expected_output = <<~RUBY 16 | gem "sidekiq" 17 | RUBY 18 | 19 | run_generator 20 | 21 | assert_file app_root("Gemfile") do |file| 22 | assert_match(expected_output, file) 23 | end 24 | end 25 | 26 | test "installs gems with Bundler" do 27 | output = run_generator 28 | 29 | assert_match(/bundle install/, output) 30 | end 31 | 32 | test "adds ActiveJob configuration to the application file" do 33 | run_generator 34 | 35 | assert_file app_root("config/application.rb") do |file| 36 | assert_match(/config.active_job.queue_adapter = :sidekiq/, file) 37 | end 38 | end 39 | 40 | test "adds ActiveJob configuration to the test environment file" do 41 | run_generator 42 | 43 | assert_file app_root("config/environments/test.rb") do |file| 44 | assert_match(/config.active_job.queue_adapter = :inline/, file) 45 | end 46 | end 47 | 48 | test "creates a Procfile.dev with Sidekiq configuration" do 49 | run_generator 50 | 51 | assert_file app_root("Procfile.dev") do |file| 52 | assert_match(/worker: bundle exec sidekiq/, file) 53 | end 54 | end 55 | 56 | test "adds Sidekiq configuration if procfile exists" do 57 | touch "Procfile.dev" 58 | 59 | run_generator 60 | 61 | assert_file app_root("Procfile.dev") do |file| 62 | assert_match(/worker: bundle exec sidekiq/, file) 63 | end 64 | end 65 | 66 | private 67 | 68 | def prepare_destination 69 | touch "Gemfile" 70 | backup_file "config/application.rb" 71 | backup_file "config/environments/test.rb" 72 | end 73 | 74 | def restore_destination 75 | remove_file_if_exists "Gemfile" 76 | remove_file_if_exists "config/initializers/active_job.rb" 77 | remove_file_if_exists "Procfile.dev" 78 | restore_file "config/application.rb" 79 | restore_file "config/environments/test.rb" 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/generators/suspenders/lint_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/lint_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class LintGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::LintGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "installs dependencies" do 15 | capture(:stderr) do 16 | output = run_generator 17 | 18 | assert_match(/yarn add stylelint eslint@\^8\.9\.0 @thoughtbot\/stylelint-config @thoughtbot\/eslint-config npm-run-all prettier --dev/, output) 19 | end 20 | end 21 | 22 | test "installs gems" do 23 | capture(:stderr) do 24 | expected_gemfile = <<~TEXT 25 | group :development, :test do 26 | gem "better_html", require: false 27 | gem "erb_lint", require: false 28 | gem "erblint-github", require: false 29 | gem "standard" 30 | end 31 | TEXT 32 | 33 | output = run_generator 34 | 35 | assert_match(/bundle install/, output) 36 | assert_file app_root "Gemfile" do |file| 37 | assert_match expected_gemfile, file 38 | end 39 | end 40 | end 41 | 42 | test "configures stylelint" do 43 | expected_content = file_fixture("stylelintrc.json").read 44 | 45 | capture(:stderr) { run_generator } 46 | 47 | assert_file app_root(".stylelintrc.json") do |file| 48 | assert_equal expected_content, file 49 | end 50 | end 51 | 52 | test "configures eslint" do 53 | expected_content = file_fixture("eslintrc.json").read 54 | 55 | capture(:stderr) { run_generator } 56 | 57 | assert_file app_root(".eslintrc.json") do |file| 58 | assert_equal expected_content, file 59 | end 60 | end 61 | 62 | test "configures prettier" do 63 | prettierrc = file_fixture("prettierrc.json").read 64 | prettierignore = file_fixture("prettierignore").read 65 | 66 | capture(:stderr) { run_generator } 67 | 68 | assert_file app_root(".prettierrc") do |file| 69 | assert_equal prettierrc, file 70 | end 71 | 72 | assert_file app_root(".prettierignore") do |file| 73 | assert_equal prettierignore, file 74 | end 75 | end 76 | 77 | test "configures erb-lint" do 78 | capture(:stderr) { run_generator } 79 | 80 | assert_file app_root(".erb-lint.yml") 81 | assert_file app_root("config/better_html.yml") 82 | assert_file app_root("config/initializers/better_html.rb") 83 | assert_file app_root("lib/tasks/erblint.rake") 84 | end 85 | 86 | test "erb-lint.yml configuration" do 87 | expected_content = file_fixture("erb-lint.yml").read 88 | 89 | capture(:stderr) { run_generator } 90 | 91 | assert_file app_root(".erb-lint.yml") do |file| 92 | assert_equal expected_content, file 93 | end 94 | end 95 | 96 | test "better html configuration" do 97 | expected_content = file_fixture("better_html.rb").read 98 | 99 | capture(:stderr) { run_generator } 100 | 101 | assert_file app_root("config/initializers/better_html.rb") do |file| 102 | assert_equal expected_content, file 103 | end 104 | end 105 | 106 | test "generates .rubocop.yml" do 107 | expected_content = <<~YAML 108 | AllCops: 109 | TargetRubyVersion: #{RUBY_VERSION} 110 | 111 | require: standard 112 | 113 | inherit_gem: 114 | standard: config/base.yml 115 | YAML 116 | 117 | capture(:stderr) { run_generator } 118 | 119 | assert_file app_root(".rubocop.yml") do |file| 120 | assert_equal expected_content, file 121 | end 122 | end 123 | 124 | test "updates package.json" do 125 | touch "package.json", content: package_json 126 | 127 | capture(:stderr) { run_generator } 128 | 129 | assert_file "package.json" do |file| 130 | assert_equal expected_package_json, file 131 | end 132 | end 133 | 134 | test "updates package.json if script key does not exist" do 135 | touch "package.json", content: package_json(empty: true) 136 | 137 | capture(:stderr) { run_generator } 138 | 139 | assert_file "package.json" do |file| 140 | assert_equal expected_package_json, file 141 | end 142 | end 143 | 144 | test "created package.json if one does not exist" do 145 | remove_file_if_exists "package.json" 146 | 147 | capture(:stderr) { run_generator } 148 | 149 | assert_file app_root("package.json") 150 | end 151 | 152 | private 153 | 154 | def prepare_destination 155 | touch "Gemfile" 156 | touch "package.json", content: package_json(empty: true) 157 | end 158 | 159 | def restore_destination 160 | remove_file_if_exists "Gemfile" 161 | remove_file_if_exists ".stylelintrc.json" 162 | remove_file_if_exists ".eslintrc.json" 163 | remove_file_if_exists ".prettierrc" 164 | remove_file_if_exists ".prettierignore" 165 | remove_file_if_exists "package.json" 166 | remove_file_if_exists ".erb-lint.yml" 167 | remove_file_if_exists "config/better_html.yml" 168 | remove_file_if_exists "config/initializers/better_html.rb" 169 | remove_file_if_exists ".rubocop.yml" 170 | remove_file_if_exists "package.json", root: true 171 | remove_file_if_exists "yarn.lock", root: true 172 | remove_dir_if_exists "lib/tasks" 173 | remove_dir_if_exists "test" 174 | remove_dir_if_exists "spec" 175 | end 176 | 177 | def package_json(empty: false) 178 | if empty 179 | <<~JSON.chomp 180 | { 181 | } 182 | JSON 183 | else 184 | <<~JSON.chomp 185 | { 186 | "scripts": {} 187 | } 188 | JSON 189 | end 190 | end 191 | 192 | def expected_package_json 193 | <<~JSON.chomp 194 | { 195 | "scripts": { 196 | "lint": "run-p lint:eslint lint:stylelint lint:prettier", 197 | "lint:eslint": "eslint --max-warnings=0 --no-error-on-unmatched-pattern 'app/javascript/**/*.js'", 198 | "lint:stylelint": "stylelint 'app/assets/stylesheets/**/*.css'", 199 | "lint:prettier": "prettier --check '**/*' --ignore-unknown", 200 | "fix:prettier": "prettier --write '**/*' --ignore-unknown" 201 | } 202 | } 203 | JSON 204 | end 205 | end 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /test/generators/suspenders/prerequisites_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "climate_control" 3 | require "generators/suspenders/prerequisites_generator" 4 | 5 | module Suspenders 6 | module Generators 7 | class PrerequisitesGeneratorTest < Rails::Generators::TestCase 8 | include Suspenders::TestHelpers 9 | 10 | tests Suspenders::Generators::PrerequisitesGenerator 11 | destination Rails.root 12 | teardown :restore_destination 13 | 14 | test "generates .node-version file (from ENV)" do 15 | ClimateControl.modify NODE_VERSION: "20.0.0" do 16 | run_generator 17 | 18 | assert_file app_root(".node-version") do |file| 19 | assert_match(/20\.0\.0/, file) 20 | end 21 | end 22 | end 23 | 24 | test "generates .node-version file (from system)" do 25 | Object.any_instance.stubs(:`).returns("v20.0.0\n") 26 | 27 | run_generator 28 | 29 | assert_file app_root(".node-version") do |file| 30 | assert_match(/20\.0\.0/, file) 31 | end 32 | end 33 | 34 | test "raises if Node is not installed" do 35 | Object.any_instance.stubs(:`).returns("") 36 | 37 | assert_raises Suspenders::Generators::NodeNotInstalled::Error do 38 | run_generator 39 | end 40 | 41 | assert_no_file app_root(".node-version") 42 | end 43 | 44 | test "raises if Node is unsupported" do 45 | Object.any_instance.stubs(:`).returns("v19.9.9\n") 46 | 47 | assert_raises Suspenders::Generators::NodeVersionUnsupported::Error do 48 | run_generator 49 | end 50 | 51 | assert_no_file app_root(".node-version") 52 | end 53 | 54 | private 55 | 56 | def restore_destination 57 | remove_file_if_exists ".node-version" 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/generators/suspenders/rake_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/rake_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class RakeGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::RakeGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "modifies existing Rakefile" do 15 | content = file_fixture("Rakefile").read 16 | 17 | run_generator 18 | 19 | assert_file app_root("Rakefile") do |file| 20 | assert_equal content, file 21 | end 22 | end 23 | 24 | private 25 | 26 | def prepare_destination 27 | touch "Gemfile" 28 | backup_file "Rakefile" 29 | end 30 | 31 | def restore_destination 32 | remove_file_if_exists "Gemfile" 33 | restore_file "Rakefile" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /test/generators/suspenders/setup_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/setup_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class SetupGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::SetupGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "modifies bin/setup" do 15 | expected = bin_setup 16 | 17 | run_generator 18 | 19 | assert_file app_root("bin/setup") do |file| 20 | assert_equal expected, file 21 | end 22 | end 23 | 24 | private 25 | 26 | def prepare_destination 27 | backup_file "bin/setup" 28 | end 29 | 30 | def restore_destination 31 | restore_file "bin/setup" 32 | end 33 | 34 | def bin_setup 35 | File.read("./lib/generators/templates/setup/bin_setup.rb") 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/generators/suspenders/styles_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/styles_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class StylesGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::StylesGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "raises if API only application" do 15 | within_api_only_app do 16 | assert_raises Suspenders::Generators::APIAppUnsupported::Error do 17 | run_generator 18 | end 19 | end 20 | end 21 | 22 | test "does not raise if API configuration is commented out" do 23 | within_api_only_app(commented_out: true) do 24 | capture(:stderr) { run_generator } 25 | end 26 | end 27 | 28 | test "adds gems to Gemfile" do 29 | capture(:stderr) { run_generator } 30 | 31 | assert_file app_root("Gemfile") do |file| 32 | assert_match "cssbundling-rails", file 33 | end 34 | end 35 | 36 | test "installs gems with Bundler" do 37 | capture(:stderr) do 38 | output = run_generator 39 | 40 | assert_match(/bundle install/, output) 41 | end 42 | end 43 | 44 | test "runs install script" do 45 | capture(:stderr) do 46 | output = run_generator 47 | 48 | assert_match(/bin\/rails css:install:postcss/, output) 49 | end 50 | end 51 | 52 | test "installs modern-normalize and imports stylesheets" do 53 | capture(:stderr) do 54 | output = run_generator 55 | application_stylesheet = <<~TEXT 56 | @import "modern-normalize"; 57 | @import "base.css"; 58 | @import "components.css"; 59 | @import "utilities.css"; 60 | TEXT 61 | 62 | assert_match(/add.*modern-normalize/, output) 63 | 64 | assert_file app_root("app/assets/stylesheets/application.postcss.css") do |file| 65 | assert_equal application_stylesheet, file 66 | end 67 | end 68 | end 69 | 70 | test "creates stylesheets" do 71 | capture(:stderr) { run_generator } 72 | 73 | assert_file app_root("app/assets/stylesheets/base.css") do |file| 74 | assert_equal "/* Base Styles */", file 75 | end 76 | assert_file app_root("app/assets/stylesheets/components.css") do |file| 77 | assert_equal "/* Component Styles */", file 78 | end 79 | assert_file app_root("app/assets/stylesheets/utilities.css") do |file| 80 | assert_equal "/* Utility Styles */", file 81 | end 82 | end 83 | 84 | test "installs postcss-url" do 85 | capture(:stderr) do 86 | output = run_generator 87 | 88 | assert_match(/add\s*postcss-url/, output) 89 | end 90 | end 91 | 92 | test "configures postcss.config.js" do 93 | expected = file_fixture("postcss.config.js").read 94 | 95 | capture(:stderr) { run_generator } 96 | 97 | assert_file app_root("postcss.config.js") do |file| 98 | assert_equal expected, file 99 | end 100 | end 101 | 102 | test "overrides existing postcss.config.js" do 103 | touch "postcss.config.js", content: "unexpected" 104 | expected = file_fixture("postcss.config.js").read 105 | 106 | capture(:stderr) { run_generator } 107 | 108 | assert_file app_root("postcss.config.js") do |file| 109 | assert_equal expected, file 110 | end 111 | end 112 | 113 | test "creates directory to store static assets generated from postcss-url" do 114 | capture(:stderr) { run_generator } 115 | 116 | assert_file app_root("app/assets/static/.gitkeep") 117 | end 118 | 119 | private 120 | 121 | def prepare_destination 122 | touch "Gemfile" 123 | touch "app/assets/stylesheets/application.postcss.css" 124 | end 125 | 126 | def restore_destination 127 | remove_file_if_exists "Gemfile" 128 | remove_file_if_exists "package.json", root: true 129 | remove_file_if_exists "yarn.lock", root: true 130 | remove_file_if_exists "app/assets/stylesheets/application.postcss.css" 131 | remove_file_if_exists "app/assets/stylesheets/base.css" 132 | remove_file_if_exists "app/assets/stylesheets/components.css" 133 | remove_file_if_exists "app/assets/stylesheets/utilities.css" 134 | remove_file_if_exists "postcss.config.js" 135 | remove_dir_if_exists "app/assets/static" 136 | end 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/generators/suspenders/tasks_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/tasks_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class TasksGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::TasksGenerator 10 | destination Rails.root 11 | teardown :restore_destination 12 | 13 | test "creates dev.rake file" do 14 | Bundler.rubygems.stubs(:find_name).with("factory_bot").returns([true]) 15 | 16 | expected = dev_rake 17 | 18 | run_generator 19 | 20 | assert_file app_root("lib/tasks/dev.rake") do |file| 21 | assert_equal expected, file 22 | end 23 | end 24 | 25 | test "returns early if factory_bot_rails is not installed" do 26 | output = run_generator 27 | 28 | assert_match(/This generator requires Factory Bot/, output) 29 | assert_no_file app_root("lib/tasks/dev.rake") 30 | end 31 | 32 | private 33 | 34 | def dev_rake 35 | File.read("./lib/generators/templates/tasks/dev.rake") 36 | end 37 | 38 | def restore_destination 39 | remove_file_if_exists "lib/tasks/dev.rake" 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/generators/suspenders/testing_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/testing_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class TestingGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::TestingGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "adds gems to Gemfile" do 15 | expected = <<~RUBY 16 | group :development, :test do 17 | gem "rspec-rails", "~> 6.1.0" 18 | end 19 | 20 | group :test do 21 | gem "capybara" 22 | gem "action_dispatch-testing-integration-capybara", github: "thoughtbot/action_dispatch-testing-integration-capybara", tag: "v0.1.1", require: "action_dispatch/testing/integration/capybara/rspec" 23 | gem "selenium-webdriver" 24 | gem "shoulda-matchers", "~> 6.0" 25 | gem "webmock" 26 | end 27 | RUBY 28 | 29 | run_generator 30 | 31 | assert_file app_root("Gemfile") do |file| 32 | assert_match(expected, file) 33 | end 34 | end 35 | 36 | test "installs gems with Bundler" do 37 | output = run_generator 38 | 39 | assert_match(/bundle install/, output) 40 | end 41 | 42 | test "runs RSpec installation script" do 43 | output = run_generator 44 | 45 | assert_match(/generate rspec:install/, output) 46 | end 47 | 48 | test "configures rails_helper" do 49 | touch "spec/rails_helper.rb", content: rails_helper 50 | 51 | run_generator 52 | 53 | assert_file "spec/rails_helper.rb" do |file| 54 | assert_match(/RSpec\.configure do \|config\|\s{3}config\.infer_base_class_for_anonymous_controllers\s*=\s*false/m, 55 | file) 56 | assert_match(/^\#{0}\s*Rails\.root\.glob\("spec\/support\/\*\*\/\*\.rb"\)\.sort\.each { \|f\| require f }/, file) 57 | end 58 | end 59 | 60 | test "configures spec_helper" do 61 | touch "spec/spec_helper.rb", content: spec_helper 62 | expected = file_fixture("spec_helper.rb").read 63 | 64 | run_generator 65 | 66 | assert_file app_root("spec/spec_helper.rb") do |file| 67 | assert_equal expected, file 68 | end 69 | end 70 | 71 | test "configures driver" do 72 | expected = file_fixture("driver.rb").read 73 | 74 | run_generator 75 | 76 | assert_file app_root("spec/support/driver.rb") do |file| 77 | assert_equal expected, file 78 | end 79 | end 80 | 81 | test "creates system spec directory" do 82 | run_generator 83 | 84 | assert_file app_root("spec/system/.gitkeep") 85 | end 86 | 87 | test "configures Should Matchers" do 88 | expected = file_fixture("shoulda_matchers.rb").read 89 | 90 | run_generator 91 | 92 | assert_file app_root("spec/support/shoulda_matchers.rb") do |file| 93 | assert_equal expected, file 94 | end 95 | end 96 | 97 | test "configures i18n" do 98 | expected = file_fixture("i18n.rb").read 99 | 100 | run_generator 101 | 102 | assert_file app_root("spec/support/i18n.rb") do |file| 103 | assert_equal expected, file 104 | end 105 | end 106 | 107 | test "configures Action Mailer" do 108 | expected = file_fixture("action_mailer.rb").read 109 | 110 | run_generator 111 | 112 | assert_file app_root("spec/support/action_mailer.rb") do |file| 113 | assert_equal expected, file 114 | end 115 | end 116 | 117 | private 118 | 119 | def prepare_destination 120 | touch "Gemfile" 121 | mkdir "spec" 122 | touch "spec/rails_helper.rb" 123 | touch "spec/spec_helper.rb" 124 | end 125 | 126 | def restore_destination 127 | remove_file_if_exists "Gemfile" 128 | remove_dir_if_exists "spec" 129 | end 130 | 131 | def rails_helper 132 | file_fixture("rails_helper.rb").read 133 | end 134 | 135 | def spec_helper 136 | file_fixture("spec_helper.rb").read 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/generators/suspenders/views_generator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "generators/suspenders/views_generator" 3 | 4 | module Suspenders 5 | module Generators 6 | class ViewsGeneratorTest < Rails::Generators::TestCase 7 | include Suspenders::TestHelpers 8 | 9 | tests Suspenders::Generators::ViewsGenerator 10 | destination Rails.root 11 | setup :prepare_destination 12 | teardown :restore_destination 13 | 14 | test "raises if API only application" do 15 | within_api_only_app do 16 | assert_raises Suspenders::Generators::APIAppUnsupported::Error do 17 | run_generator 18 | end 19 | 20 | assert_file app_root("Gemfile") do |file| 21 | assert_no_match "title", file 22 | end 23 | end 24 | end 25 | 26 | test "creates flash partial" do 27 | expected = file_fixture("_flashes.html.erb").read 28 | 29 | run_generator 30 | 31 | assert_file app_root("app/views/application/_flashes.html.erb") do |file| 32 | assert_equal expected, file 33 | end 34 | end 35 | 36 | test "includes flash partial in layout" do 37 | run_generator 38 | 39 | assert_file app_root("app/views/layouts/application.html.erb") do |file| 40 | assert_match(/\s{5}<%= render "flashes" -%>$/, file) 41 | end 42 | end 43 | 44 | test "sets the language" do 45 | run_generator 46 | 47 | assert_file app_root("app/views/layouts/application.html.erb") do |file| 48 | assert_match(//, file) 49 | end 50 | end 51 | 52 | test "adds gems to Gemfile" do 53 | expected_output = <<~RUBY 54 | gem "title" 55 | RUBY 56 | 57 | run_generator 58 | 59 | assert_file app_root("Gemfile") do |file| 60 | assert_match(expected_output, file) 61 | end 62 | end 63 | 64 | test "installs gems with Bundler" do 65 | output = run_generator 66 | 67 | assert_match(/bundle install/, output) 68 | end 69 | 70 | test "sets title" do 71 | run_generator 72 | 73 | assert_file app_root("app/views/layouts/application.html.erb") do |file| 74 | assert_match(/<%= title %><\/title>/, file) 75 | end 76 | end 77 | 78 | test "disables InstantClick" do 79 | run_generator 80 | 81 | assert_file app_root("app/views/layouts/application.html.erb") do |file| 82 | assert_match(/<head>.*<\/title>\s{4}<meta\s*name="turbo-prefetch"\s*content=\s*"false">/m, file) 83 | end 84 | end 85 | 86 | private 87 | 88 | def prepare_destination 89 | touch "Gemfile" 90 | backup_file "app/views/layouts/application.html.erb" 91 | end 92 | 93 | def restore_destination 94 | remove_file_if_exists "Gemfile" 95 | remove_file_if_exists "app/views/application/_flashes.html.erb" 96 | restore_file "app/views/layouts/application.html.erb" 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/suspenders/cleanup/generate_readme_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "tempfile" 3 | require_relative "../../../lib/suspenders/cleanup/generate_readme" 4 | 5 | module Suspenders 6 | module Cleanup 7 | class GenerateReadmeTest < ActiveSupport::TestCase 8 | test "generates README using generator descriptions" do 9 | Object.any_instance.stubs(:`).returns("v20.0.0\n") 10 | 11 | Tempfile.create "README.md" do |readme| 12 | path = readme.path 13 | 14 | Suspenders::Cleanup::GenerateReadme.perform(path, "ExpectedAppName") 15 | 16 | readme.rewind 17 | readme = readme.read 18 | 19 | assert_match "# Expected App Name", readme 20 | 21 | assert_match "## Prerequisites", readme 22 | assert_match Suspenders::MINIMUM_RUBY_VERSION, readme 23 | assert_match "Node: `20.0.0`", readme 24 | 25 | assert_match "## Configuration", readme 26 | assert_match "### Test", readme 27 | assert_match Suspenders::Generators::Environments::TestGenerator.desc, readme 28 | assert_match "### Development", readme 29 | assert_match Suspenders::Generators::Environments::DevelopmentGenerator.desc, readme 30 | assert_match "### Production", readme 31 | assert_match Suspenders::Generators::Environments::ProductionGenerator.desc, readme 32 | 33 | assert_match "### Linting", readme 34 | assert_match Suspenders::Generators::LintGenerator.desc, readme 35 | 36 | assert_match "## Testing", readme 37 | assert_match Suspenders::Generators::TestingGenerator.desc, readme 38 | assert_match "### Factories", readme 39 | assert_match Suspenders::Generators::FactoriesGenerator.desc, readme 40 | 41 | assert_match "## Accessibility", readme 42 | assert_match Suspenders::Generators::AccessibilityGenerator.desc, readme 43 | 44 | assert_match "## Advisories", readme 45 | assert_match Suspenders::Generators::AdvisoriesGenerator.desc, readme 46 | 47 | assert_match "## Mailers", readme 48 | assert_match Suspenders::Generators::EmailGenerator.desc, readme 49 | 50 | assert_match "## Jobs", readme 51 | assert_match Suspenders::Generators::JobsGenerator.desc, readme 52 | 53 | assert_match "## Layout and Assets", readme 54 | 55 | assert_match "### Stylesheets", readme 56 | assert_match Suspenders::Generators::StylesGenerator.desc, readme 57 | assert_match "### Inline SVG", readme 58 | assert_match Suspenders::Generators::InlineSvgGenerator.desc, readme 59 | assert_match "### Layout", readme 60 | assert_match Suspenders::Generators::ViewsGenerator.desc, readme 61 | end 62 | end 63 | 64 | test "replaces existing README" do 65 | Tempfile.create "README.md" do |readme| 66 | path = readme.path 67 | readme.write "Unexpected Content" 68 | readme.rewind 69 | 70 | Suspenders::Cleanup::GenerateReadme.perform(path, "App") 71 | 72 | readme.rewind 73 | readme = readme.read 74 | 75 | assert_no_match "Unexpected Content", readme 76 | end 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/suspenders/cleanup/organize_gemfile_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "tempfile" 3 | require_relative "../../../lib/suspenders/cleanup/organize_gemfile" 4 | 5 | module Suspenders 6 | module Cleanup 7 | class OrganizeGemfileTest < ActiveSupport::TestCase 8 | test "organizes Gemfile by group" do 9 | original = file_fixture("gemfile_messy").read 10 | modified = file_fixture("gemfile_clean").read 11 | 12 | Tempfile.create "Gemfile" do |gemfile| 13 | gemfile.write original 14 | gemfile.rewind 15 | 16 | Suspenders::Cleanup::OrganizeGemfile.perform(gemfile.path) 17 | 18 | assert_equal modified, gemfile.read 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/suspenders/generators_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class Suspenders::GeneratorsTest < ActiveSupport::TestCase 4 | class APIAppUnsupportedTest < ActiveSupport::TestCase 5 | test "message returns a custom message" do 6 | expected = "This generator cannot be used on API only applications." 7 | 8 | assert_equal expected, Suspenders::Generators::APIAppUnsupported::Error.new.message 9 | end 10 | end 11 | 12 | class DatabaseUnsupportedTest < ActiveSupport::TestCase 13 | test "message returns a custom message" do 14 | expected = "This generator requires PostgreSQL" 15 | 16 | assert_equal expected, Suspenders::Generators::DatabaseUnsupported::Error.new.message 17 | end 18 | end 19 | 20 | class NodeNotInstalledTest < ActiveSupport::TestCase 21 | test "message returns a custom message" do 22 | expected = "This generator requires Node" 23 | 24 | assert_equal expected, Suspenders::Generators::NodeNotInstalled::Error.new.message 25 | end 26 | end 27 | 28 | class NodeVersionUnsupportedTest < ActiveSupport::TestCase 29 | test "message returns a custom message" do 30 | expected = "This generator requires Node >= #{Suspenders::MINIMUM_NODE_VERSION}" 31 | 32 | assert_equal expected, Suspenders::Generators::NodeVersionUnsupported::Error.new.message 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/suspenders_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SuspendersTest < ActiveSupport::TestCase 4 | test "it has a version number" do 5 | assert Suspenders::VERSION 6 | end 7 | 8 | test "it has a Rails version number" do 9 | assert Suspenders::RAILS_VERSION 10 | end 11 | 12 | test "it has a Minimum Ruby version number" do 13 | assert Suspenders::MINIMUM_RUBY_VERSION 14 | end 15 | 16 | test "it has a Minimum Node version number" do 17 | assert Suspenders::MINIMUM_NODE_VERSION 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Configure Rails Environment 2 | ENV["RAILS_ENV"] = "test" 3 | 4 | require_relative "../test/dummy/config/environment" 5 | ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)] 6 | require "rails/test_help" 7 | require "mocha/minitest" 8 | 9 | # Load fixtures from the engine 10 | if ActiveSupport::TestCase.respond_to?(:fixture_paths=) 11 | ActiveSupport::TestCase.fixture_paths = [File.expand_path("fixtures", __dir__)] 12 | ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths 13 | ActiveSupport::TestCase.file_fixture_path = File.expand_path("fixtures", __dir__) + "/files" 14 | ActiveSupport::TestCase.fixtures :all 15 | end 16 | 17 | module Suspenders::TestHelpers 18 | def app_root(path) 19 | Rails.root.join path 20 | end 21 | 22 | def remove_file_if_exists(file, **options) 23 | root = options[:root] 24 | path = root ? file : app_root(file) 25 | 26 | FileUtils.rm path if File.exist? path 27 | end 28 | 29 | def remove_dir_if_exists(dir) 30 | path = app_root dir 31 | 32 | FileUtils.rm_r path if File.exist? path 33 | end 34 | 35 | def mkdir(dir) 36 | path = app_root dir 37 | 38 | FileUtils.mkdir_p path 39 | end 40 | 41 | def touch(file, **options) 42 | content = options[:content] 43 | path = app_root file 44 | 45 | FileUtils.touch path 46 | 47 | if content 48 | File.write app_root(path), content 49 | end 50 | end 51 | 52 | def within_api_only_app(**options, &block) 53 | commented_out = options[:commented_out] 54 | set_config = if commented_out == true 55 | "# config.api_only = true" 56 | else 57 | "config.api_only = true" 58 | end 59 | 60 | backup_file "config/application.rb" 61 | application_config = <<~RUBY 62 | require_relative "boot" 63 | require "rails/all" 64 | 65 | Bundler.require(*Rails.groups) 66 | 67 | module Dummy 68 | class Application < Rails::Application 69 | config.load_defaults 7.1 70 | 71 | config.autoload_lib(ignore: %w(assets tasks)) 72 | 73 | #{set_config} 74 | end 75 | end 76 | RUBY 77 | File.open(app_root("config/application.rb"), "w") { _1.write application_config } 78 | 79 | yield 80 | ensure 81 | restore_file "config/application.rb" 82 | end 83 | 84 | def with_test_suite(test_suite, &block) 85 | case test_suite 86 | when :minitest 87 | mkdir "test" 88 | touch "test/test_helper.rb", content: file_fixture("test_helper.rb").read 89 | when :rspec 90 | mkdir "spec" 91 | touch "spec/spec_helper.rb" 92 | else 93 | raise ArgumentError, "unknown test suite: #{test_suite.inspect}" 94 | end 95 | 96 | yield 97 | ensure 98 | remove_dir_if_exists "test" 99 | remove_dir_if_exists "spec" 100 | end 101 | 102 | def with_database(database, &block) 103 | backup_file "config/database.yml" 104 | configuration = File.read app_root("config/database.yml") 105 | configuration = YAML.safe_load(configuration, aliases: true) 106 | configuration["default"]["adapter"] = database 107 | File.open(app_root("config/database.yml"), "w") { _1.write configuration.to_yaml } 108 | 109 | yield 110 | ensure 111 | restore_file "config/database.yml" 112 | end 113 | 114 | def backup_file(file) 115 | FileUtils.copy app_root(file), app_root("#{file}.bak") 116 | end 117 | 118 | def restore_file(file) 119 | remove_file_if_exists(file) 120 | FileUtils.mv app_root("#{file}.bak"), app_root(file) 121 | end 122 | end 123 | --------------------------------------------------------------------------------