From bb098e40cf46a59c246fdf6f4b138ca533c376d9 Mon Sep 17 00:00:00 2001 From: Steve Polito Date: Tue, 9 Apr 2024 20:08:43 -0400 Subject: [PATCH] Dynamically generate README Create README based off the `desc` of the generators. We chose to create a custom class instead of a traditional generator because this script is intended to be run in the [application template][] as a Rake task. It is not intended to be run as a standalone generator, since it only makes sense to be run on concert with `suspenders:install:web`. Updates all generator descriptions to use Herdoc syntax to ensure consistent line breaks. [application template]: https://guides.rubyonrails.org/rails_application_templates.html --- .../suspenders/accessibility_generator.rb | 4 +- .../suspenders/advisories_generator.rb | 4 +- lib/generators/suspenders/ci_generator.rb | 4 +- lib/generators/suspenders/email_generator.rb | 4 +- .../suspenders/factories_generator.rb | 4 +- .../suspenders/inline_svg_generator.rb | 4 +- lib/generators/suspenders/jobs_generator.rb | 4 +- lib/generators/suspenders/lint_generator.rb | 4 +- .../suspenders/prerequisites_generator.rb | 4 +- lib/generators/suspenders/rake_generator.rb | 4 +- lib/generators/suspenders/setup_generator.rb | 4 +- lib/generators/suspenders/styles_generator.rb | 4 +- lib/generators/suspenders/tasks_generator.rb | 4 +- .../suspenders/testing_generator.rb | 4 +- lib/generators/suspenders/views_generator.rb | 4 +- lib/suspenders.rb | 1 + lib/suspenders/cleanup/generate_readme.rb | 151 ++++++++++++++++++ lib/tasks/suspenders.rake | 5 + .../cleanup/generate_readme_test.rb | 80 ++++++++++ 19 files changed, 275 insertions(+), 22 deletions(-) create mode 100644 lib/suspenders/cleanup/generate_readme.rb create mode 100644 test/suspenders/cleanup/generate_readme_test.rb diff --git a/lib/generators/suspenders/accessibility_generator.rb b/lib/generators/suspenders/accessibility_generator.rb index ea6da16b6..003ac817d 100644 --- a/lib/generators/suspenders/accessibility_generator.rb +++ b/lib/generators/suspenders/accessibility_generator.rb @@ -3,7 +3,9 @@ module Generators class AccessibilityGenerator < Rails::Generators::Base include Suspenders::Generators::APIAppUnsupported - desc "Installs capybara_accessibility_audit and capybara_accessible_selectors" + desc <<~MARKDOWN + Installs capybara_accessibility_audit and capybara_accessible_selectors" + MARKDOWN def add_capybara_gems gem_group :test do diff --git a/lib/generators/suspenders/advisories_generator.rb b/lib/generators/suspenders/advisories_generator.rb index a2492319f..2d27fca50 100644 --- a/lib/generators/suspenders/advisories_generator.rb +++ b/lib/generators/suspenders/advisories_generator.rb @@ -2,12 +2,12 @@ module Suspenders module Generators class AdvisoriesGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/advisories", __FILE__) - desc(<<~TEXT) + desc <<~MARKDOWN Show security advisories during development. Uses the `bundler-audit` gem to update the local security database and show any relevant issues with the app's dependencies via a Rake task. - TEXT + MARKDOWN def add_bundler_audit gem_group :development, :test do diff --git a/lib/generators/suspenders/ci_generator.rb b/lib/generators/suspenders/ci_generator.rb index eec4b0f48..4204b38d0 100644 --- a/lib/generators/suspenders/ci_generator.rb +++ b/lib/generators/suspenders/ci_generator.rb @@ -5,7 +5,9 @@ class CiGenerator < Rails::Generators::Base include Suspenders::Generators::Helpers source_root File.expand_path("../../templates/ci", __FILE__) - desc "Creates CI files for GitHub Actions" + desc <<~MARKDOWN + Creates CI files for GitHub Actions + MARKDOWN def ci_files empty_directory ".github/workflows" diff --git a/lib/generators/suspenders/email_generator.rb b/lib/generators/suspenders/email_generator.rb index f00e3bf82..7883b692a 100644 --- a/lib/generators/suspenders/email_generator.rb +++ b/lib/generators/suspenders/email_generator.rb @@ -2,7 +2,7 @@ module Suspenders module Generators class EmailGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/email", __FILE__) - desc <<~TEXT + desc <<~MARKDOWN Intercepts emails in non-production environments by setting `INTERCEPTOR_ADDRESSES`. ```sh @@ -10,7 +10,7 @@ class EmailGenerator < Rails::Generators::Base ``` Configures `default_url_options` in `test` and `development`. - TEXT + MARKDOWN def create_email_interceptor copy_file "email_interceptor.rb", "app/mailers/email_interceptor.rb" diff --git a/lib/generators/suspenders/factories_generator.rb b/lib/generators/suspenders/factories_generator.rb index f471fb0fb..855b82984 100644 --- a/lib/generators/suspenders/factories_generator.rb +++ b/lib/generators/suspenders/factories_generator.rb @@ -4,7 +4,7 @@ class FactoriesGenerator < Rails::Generators::Base include Suspenders::Generators::Helpers source_root File.expand_path("../../templates/factories", __FILE__) - desc <<~TEXT + desc <<~MARKDOWN Build test data with clarity and ease. This uses FactoryBot to help you define dummy and test data for your test @@ -17,7 +17,7 @@ class FactoriesGenerator < Rails::Generators::Base definitions. Supports the default test suite and RSpec. - TEXT + MARKDOWN def add_factory_bot gem_group :development, :test do diff --git a/lib/generators/suspenders/inline_svg_generator.rb b/lib/generators/suspenders/inline_svg_generator.rb index cfd5a4d0e..0d530aaef 100644 --- a/lib/generators/suspenders/inline_svg_generator.rb +++ b/lib/generators/suspenders/inline_svg_generator.rb @@ -3,7 +3,9 @@ module Generators class InlineSvgGenerator < Rails::Generators::Base include Suspenders::Generators::APIAppUnsupported source_root File.expand_path("../../templates/inline_svg", __FILE__) - desc "Render SVG images inline, as a potential performance improvement for the viewer." + desc <<~MARKDOWN + Render SVG images inline, as a potential performance improvement for the viewer. + MARKDOWN def add_inline_svg_gem gem "inline_svg" diff --git a/lib/generators/suspenders/jobs_generator.rb b/lib/generators/suspenders/jobs_generator.rb index d4fa42c53..5657a029d 100644 --- a/lib/generators/suspenders/jobs_generator.rb +++ b/lib/generators/suspenders/jobs_generator.rb @@ -2,7 +2,9 @@ module Suspenders module Generators class JobsGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/active_job", __FILE__) - desc "Installs Sidekiq for background job processing." + desc <<~MARKDOWN + Installs Sidekiq for background job processing. + MARKDOWN def add_sidekiq_gem gem "sidekiq" diff --git a/lib/generators/suspenders/lint_generator.rb b/lib/generators/suspenders/lint_generator.rb index 21c95e4ff..abd00e0c6 100644 --- a/lib/generators/suspenders/lint_generator.rb +++ b/lib/generators/suspenders/lint_generator.rb @@ -4,7 +4,9 @@ class LintGenerator < Rails::Generators::Base include Suspenders::Generators::Helpers source_root File.expand_path("../../templates/lint", __FILE__) - desc "Creates a holistic linting solution that covers JavaScript, CSS, Ruby and ERB." + desc <<~MARKDOWN + Creates a holistic linting solution that covers JavaScript, CSS, Ruby and ERB. + MARKDOWN def check_package_json unless File.exist? Rails.root.join("package.json") diff --git a/lib/generators/suspenders/prerequisites_generator.rb b/lib/generators/suspenders/prerequisites_generator.rb index 1063b4b8f..59f84d78d 100644 --- a/lib/generators/suspenders/prerequisites_generator.rb +++ b/lib/generators/suspenders/prerequisites_generator.rb @@ -3,7 +3,9 @@ module Generators class PrerequisitesGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/prerequisites", __FILE__) - desc "Configures prerequisites. Currently Node." + desc <<~MARKDOWN + Configures prerequisites. Currently Node. + MARKDOWN def node_version template "node-version", ".node-version" diff --git a/lib/generators/suspenders/rake_generator.rb b/lib/generators/suspenders/rake_generator.rb index d8ca3afba..7e84033e3 100644 --- a/lib/generators/suspenders/rake_generator.rb +++ b/lib/generators/suspenders/rake_generator.rb @@ -2,10 +2,10 @@ module Suspenders module Generators class RakeGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/rake", __FILE__) - desc(<<~TEXT) + desc <<~MARKDOWN Configures the default Rake task to audit and lint the codebase with `bundler-audit` and `standard`, in addition to running the test suite. - TEXT + MARKDOWN def configure_default_rake_task append_to_file "Rakefile", <<~RUBY diff --git a/lib/generators/suspenders/setup_generator.rb b/lib/generators/suspenders/setup_generator.rb index 7a9fae116..3f69e499e 100644 --- a/lib/generators/suspenders/setup_generator.rb +++ b/lib/generators/suspenders/setup_generator.rb @@ -2,13 +2,13 @@ module Suspenders module Generators class SetupGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/setup", __FILE__) - desc <<~TEXT + desc <<~MARKDOWN A holistic setup script. ```sh bin/setup ``` - TEXT + MARKDOWN def replace_bin_setup copy_file "bin_setup.rb", "bin/setup", force: true diff --git a/lib/generators/suspenders/styles_generator.rb b/lib/generators/suspenders/styles_generator.rb index 84220a752..a4703a223 100644 --- a/lib/generators/suspenders/styles_generator.rb +++ b/lib/generators/suspenders/styles_generator.rb @@ -3,11 +3,11 @@ module Generators class StylesGenerator < Rails::Generators::Base include Suspenders::Generators::APIAppUnsupported - desc <<~TEXT + desc <<~MARKDOWN Configures application to use PostCSS via cssbundling-rails. Adds modern-normalize, and style sheet structure. - TEXT + MARKDOWN def add_cssbundling_rails_gem gem "cssbundling-rails" diff --git a/lib/generators/suspenders/tasks_generator.rb b/lib/generators/suspenders/tasks_generator.rb index 2057e6603..b8b9a8a7b 100644 --- a/lib/generators/suspenders/tasks_generator.rb +++ b/lib/generators/suspenders/tasks_generator.rb @@ -2,11 +2,11 @@ module Suspenders module Generators class TasksGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/tasks", __FILE__) - desc <<~TEXT + desc <<~MARKDOWN Creates local Rake tasks for development bin/rails dev:prime # Sample data for local development environment - TEXT + MARKDOWN def create_dev_rake if Bundler.rubygems.find_name("factory_bot").any? diff --git a/lib/generators/suspenders/testing_generator.rb b/lib/generators/suspenders/testing_generator.rb index 31729c025..cc862055a 100644 --- a/lib/generators/suspenders/testing_generator.rb +++ b/lib/generators/suspenders/testing_generator.rb @@ -2,7 +2,9 @@ module Suspenders module Generators class TestingGenerator < Rails::Generators::Base source_root File.expand_path("../../templates/testing", __FILE__) - desc "Set up the project for an in-depth test-driven development workflow." + desc <<~MARKDOWN + Set up the project for an in-depth test-driven development workflow. + MARKDOWN def add_gems gem_group :development, :test do diff --git a/lib/generators/suspenders/views_generator.rb b/lib/generators/suspenders/views_generator.rb index 2c3d14a0d..227ce3b75 100644 --- a/lib/generators/suspenders/views_generator.rb +++ b/lib/generators/suspenders/views_generator.rb @@ -3,7 +3,9 @@ module Generators class ViewsGenerator < Rails::Generators::Base include Suspenders::Generators::APIAppUnsupported - desc "Configures flash messages, page titles and the document lang. Disables Turbo's InstantClick." + desc <<~MARKDOWN + Configures flash messages, page titles and the document lang. Disables Turbo's InstantClick. + MARKDOWN source_root File.expand_path("../../templates/views", __FILE__) def install_gems diff --git a/lib/suspenders.rb b/lib/suspenders.rb index 406303a32..5042db8bf 100644 --- a/lib/suspenders.rb +++ b/lib/suspenders.rb @@ -3,6 +3,7 @@ require "suspenders/railtie" require "suspenders/generators" require "suspenders/cleanup/organize_gemfile" +require "suspenders/cleanup/generate_readme" module Suspenders # Your code goes here... diff --git a/lib/suspenders/cleanup/generate_readme.rb b/lib/suspenders/cleanup/generate_readme.rb new file mode 100644 index 000000000..981377b87 --- /dev/null +++ b/lib/suspenders/cleanup/generate_readme.rb @@ -0,0 +1,151 @@ +require "rails/generators" +require_relative "../../generators/suspenders/environments/development_generator" +require_relative "../../generators/suspenders/environments/test_generator" +require_relative "../../generators/suspenders/environments/production_generator" +require_relative "../../generators/suspenders/accessibility_generator" +require_relative "../../generators/suspenders/advisories_generator" +require_relative "../../generators/suspenders/email_generator" +require_relative "../../generators/suspenders/factories_generator" +require_relative "../../generators/suspenders/inline_svg_generator" +require_relative "../../generators/suspenders/jobs_generator" +require_relative "../../generators/suspenders/lint_generator" +require_relative "../../generators/suspenders/styles_generator" +require_relative "../../generators/suspenders/testing_generator" +require_relative "../../generators/suspenders/views_generator" + +module Suspenders + module Cleanup + class GenerateReadme + def self.perform(readme, app_name) + new(readme, app_name).perform + end + + attr_reader :readme, :app_name + + def initialize(readme, app_name) + @readme = readme + @app_name = app_name + end + + def perform + File.open(readme, "w+") do |file| + @file = file + + heading app_name, level: 1 + + prerequisites + + local_development + + heading "Configuration", level: 2 + + heading "Test", level: 3 + description_for Suspenders::Generators::Environments::TestGenerator + + heading "Development", level: 3 + description_for Suspenders::Generators::Environments::DevelopmentGenerator + + heading "Production", level: 3 + description_for Suspenders::Generators::Environments::ProductionGenerator + + heading "Linting", level: 3 + description_for Suspenders::Generators::LintGenerator + + heading "Testing", level: 2 + description_for Suspenders::Generators::TestingGenerator + + heading "Factories", level: 3 + description_for Suspenders::Generators::FactoriesGenerator + + heading "Accessibility", level: 2 + description_for Suspenders::Generators::AccessibilityGenerator + + heading "Advisories", level: 2 + description_for Suspenders::Generators::AdvisoriesGenerator + + heading "Mailers", level: 2 + description_for Suspenders::Generators::EmailGenerator + + heading "Jobs", level: 2 + description_for Suspenders::Generators::JobsGenerator + + heading "Layout and Assets", level: 2 + + heading "Stylesheets", level: 3 + description_for Suspenders::Generators::StylesGenerator + + heading "Inline SVG", level: 3 + description_for Suspenders::Generators::InlineSvgGenerator + + heading "Layout", level: 3 + description_for Suspenders::Generators::ViewsGenerator + end + end + + private + + def new_line + @file.write "\n" + end + + def heading(text, level:) + @file.write "#{"#" * level} #{text}\n" + new_line + end + + def description_for(generator) + @file.write generator.desc + new_line + end + + def local_development + @file.write <<~MARKDOWN + ## Local Development + + ### Initial Setup + + ``` + bin/setup + ``` + + ### Running the Development Server + + ``` + bin/rails dev + ``` + + ### Seed Data + + - Use `db/seeds.rb` to create records that need to exist in all environments. + - Use `lib/tasks/dev.rake` to create records that only need to exist in development. + + Running `bin/setup` will run `dev:prime`. + + ### Tasks + + - Use `bin/rails suspenders:db:migrate` to run [database migrations][]. This script ensures they are [reversible][]. + - Use `bin/rails suspenders:cleanup:organize_gemfile` to automatically organize the project's Gemfile. + - Use `bin/rails default` to run the default Rake task. This will do the following: + - Run the test suite. + - Run a Ruby and ERB linter. + - Scan the Ruby codebase for any dependecy vulnerabilities. + + [database migrations]: https://edgeguides.rubyonrails.org/active_record_migrations.html#running-migrations + [reversible]: https://edgeguides.rubyonrails.org/active_record_migrations.html#making-the-irreversible-possible + + MARKDOWN + end + + def prerequisites + heading "Prerequisites", level: 2 + + @file.write <<~MARKDOWN + Ruby: `#{Suspenders::MINIMUM_RUBY_VERSION}` + Node: `#{Suspenders::NODE_LTS_VERSION}` + MARKDOWN + + new_line + end + end + end +end diff --git a/lib/tasks/suspenders.rake b/lib/tasks/suspenders.rake index da9a42ae2..fb0122cb9 100644 --- a/lib/tasks/suspenders.rake +++ b/lib/tasks/suspenders.rake @@ -28,5 +28,10 @@ namespace :suspenders do task :organize_gemfile do Suspenders::Cleanup::OrganizeGemfile.perform(Rails.root.join("Gemfile")) end + + desc "Generate README" + task :generate_readme do + Suspenders::Cleanup::GenerateReadme.perform(Rails.root.join("README.md"), Rails.application.class.module_parent_name) + end end end diff --git a/test/suspenders/cleanup/generate_readme_test.rb b/test/suspenders/cleanup/generate_readme_test.rb new file mode 100644 index 000000000..50b2b80fb --- /dev/null +++ b/test/suspenders/cleanup/generate_readme_test.rb @@ -0,0 +1,80 @@ +require "test_helper" +require "tempfile" +require_relative "../../../lib/suspenders/cleanup/generate_readme" + +module Suspenders + module Cleanup + class GenerateReadmeTest < ActiveSupport::TestCase + test "generates README using generator descriptions" do + Tempfile.create "README" do |readme| + path = readme.path + + Suspenders::Cleanup::GenerateReadme.perform(path, "Expected App Name") + + readme.rewind + readme = readme.read + + puts readme + + assert_match "# Expected App Name", readme + + assert_match "## Prerequisites", readme + assert_match Suspenders::MINIMUM_RUBY_VERSION, readme + assert_match Suspenders::NODE_LTS_VERSION, readme + + assert_match "## Configuration", readme + assert_match "### Test", readme + assert_match Suspenders::Generators::Environments::TestGenerator.desc, readme + assert_match "### Development", readme + assert_match Suspenders::Generators::Environments::DevelopmentGenerator.desc, readme + assert_match "### Production", readme + assert_match Suspenders::Generators::Environments::ProductionGenerator.desc, readme + + assert_match "### Linting", readme + assert_match Suspenders::Generators::LintGenerator.desc, readme + + assert_match "## Testing", readme + assert_match Suspenders::Generators::TestingGenerator.desc, readme + assert_match "### Factories", readme + assert_match Suspenders::Generators::FactoriesGenerator.desc, readme + + assert_match "## Accessibility", readme + assert_match Suspenders::Generators::AccessibilityGenerator.desc, readme + + assert_match "## Advisories", readme + assert_match Suspenders::Generators::AdvisoriesGenerator.desc, readme + + assert_match "## Mailers", readme + assert_match Suspenders::Generators::EmailGenerator.desc, readme + + assert_match "## Jobs", readme + assert_match Suspenders::Generators::JobsGenerator.desc, readme + + assert_match "## Layout and Assets", readme + + assert_match "### Stylesheets", readme + assert_match Suspenders::Generators::StylesGenerator.desc, readme + assert_match "### Inline SVG", readme + assert_match Suspenders::Generators::InlineSvgGenerator.desc, readme + assert_match "### Layout", readme + assert_match Suspenders::Generators::ViewsGenerator.desc, readme + end + end + + test "replaces existing README" do + Tempfile.create "README" do |readme| + path = readme.path + readme.write "Unexpected Content" + readme.rewind + + Suspenders::Cleanup::GenerateReadme.perform(path, "App") + + readme.rewind + readme = readme.read + + assert_no_match "Unexpected Content", readme + end + end + end + end +end