diff --git a/README.md b/README.md index 8a9110e..ce09986 100644 --- a/README.md +++ b/README.md @@ -146,20 +146,18 @@ puts result.issues.to_a.map(&:message) ## Rails Usage -For an app that uses Crawlscope through rake tasks, load the task entrypoint -only when a Crawlscope task is requested. In the application's `Rakefile`, -before `require_relative "config/application"`: +Run the install generator after adding the gem: -```ruby -list_tasks = Rake.application.options.show_tasks || ARGV.any? { |argument| ["-T", "--tasks"].include?(argument) } -require "crawlscope/tasks" if list_tasks || ARGV.any? { |argument| argument.start_with?("crawlscope:") } +```bash +bin/rails generate crawlscope:install ``` -This preserves `bin/rails --tasks` and the `crawlscope:*` tasks without loading -Crawlscope during normal Rails boot. Runtime callers should explicitly -`require "crawlscope"` immediately before configuring or using the gem. +The generator creates `config/initializers/crawlscope.rb` with an idempotent +`CrawlscopeConfiguration.apply` loader and adds conditional `crawlscope/tasks` +loading to the application's `Rakefile`. This preserves `bin/rails --tasks` and +the `crawlscope:*` tasks without loading Crawlscope during normal Rails boot. -In an initializer: +Customize the `Crawlscope.configure` block inside the generated initializer: ```ruby Crawlscope.configure do |config| @@ -170,6 +168,17 @@ Crawlscope.configure do |config| end ``` +Runtime callers must apply the generated configuration before using +Crawlscope: + +```ruby +CrawlscopeConfiguration.apply +Crawlscope.configuration.audit +``` + +Rake tasks apply it automatically because `crawlscope/tasks` loads the gem +before Rails evaluates the initializer. + Then run: ```bash diff --git a/lib/crawlscope.rb b/lib/crawlscope.rb index ea32c9b..192cbd0 100644 --- a/lib/crawlscope.rb +++ b/lib/crawlscope.rb @@ -27,6 +27,7 @@ def reset! end Crawlscope.instance_variable_set(:@loader, Zeitwerk::Loader.for_gem) +Crawlscope.loader.ignore("#{__dir__}/generators") Crawlscope.loader.ignore("#{__dir__}/tasks") Crawlscope.loader.ignore("#{__dir__}/crawlscope/railtie.rb") Crawlscope.loader.ignore("#{__dir__}/crawlscope/tasks.rb") diff --git a/lib/generators/crawlscope/install_generator.rb b/lib/generators/crawlscope/install_generator.rb new file mode 100644 index 0000000..6bd9957 --- /dev/null +++ b/lib/generators/crawlscope/install_generator.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "rails/generators" + +module Crawlscope + module Generators + class InstallGenerator < Rails::Generators::Base + source_root File.expand_path("templates", __dir__) + + desc "Create a lazy-safe Crawlscope initializer and task loader" + + def create_initializer + template "initializer.rb.tt", "config/initializers/crawlscope.rb" + end + + def configure_rakefile + rakefile = File.join(destination_root, "Rakefile") + return unless File.exist?(rakefile) + return if File.read(rakefile).include?('require "crawlscope/tasks"') + + inject_into_file "Rakefile", rakefile_setup, before: /^require_relative ["']config\/application["']/ + end + + private + + def rakefile_setup + <<~RUBY + crawlscope_tasks_requested = Rake.application.options.show_tasks || + ARGV.any? { |argument| ["-T", "--tasks"].include?(argument) } + require "crawlscope/tasks" if crawlscope_tasks_requested || + ARGV.any? { |argument| argument.start_with?("crawlscope:") } + + RUBY + end + end + end +end diff --git a/lib/generators/crawlscope/templates/initializer.rb.tt b/lib/generators/crawlscope/templates/initializer.rb.tt new file mode 100644 index 0000000..1a18826 --- /dev/null +++ b/lib/generators/crawlscope/templates/initializer.rb.tt @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module CrawlscopeConfiguration + MUTEX = Mutex.new + + def self.apply + return if @applied + + MUTEX.synchronize do + return if @applied + + require "crawlscope" + + Crawlscope.configure do |config| + config.base_url = -> { ENV.fetch("CRAWLSCOPE_BASE_URL", "http://localhost:3000") } + config.sitemap_path = lambda { + ENV.fetch("SITEMAP", Rails.public_path.join("sitemap.xml").to_s) + } + config.site_name = ENV.fetch("CRAWLSCOPE_SITE_NAME", "Application") + end + + @applied = true + end + end +end + +CrawlscopeConfiguration.apply if defined?(Crawlscope) diff --git a/test/crawlscope/install_generator_test.rb b/test/crawlscope/install_generator_test.rb new file mode 100644 index 0000000..73f9073 --- /dev/null +++ b/test/crawlscope/install_generator_test.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "test_helper" +require "generators/crawlscope/install_generator" +require "open3" +require "rbconfig" + +class CrawlscopeInstallGeneratorTest < Minitest::Test + def test_generates_lazy_initializer_and_task_loader + Dir.mktmpdir do |destination| + File.write(File.join(destination, "Rakefile"), <<~RUBY) + require_relative "config/application" + Rails.application.load_tasks + RUBY + + Crawlscope::Generators::InstallGenerator.start([], destination_root: destination) + + initializer = File.join(destination, "config/initializers/crawlscope.rb") + rakefile = File.read(File.join(destination, "Rakefile")) + + assert File.exist?(initializer) + assert_includes File.read(initializer), "module CrawlscopeConfiguration" + assert_includes File.read(initializer), "CrawlscopeConfiguration.apply if defined?(Crawlscope)" + assert_includes rakefile, 'require "crawlscope/tasks"' + assert_operator rakefile.index('require "crawlscope/tasks"'), :<, rakefile.index('require_relative "config/application"') + + _stdout, stderr, status = Open3.capture3(RbConfig.ruby, initializer) + assert status.success?, stderr + + script = <<~RUBY + require "pathname" + + module Rails + def self.public_path + Pathname.new("public") + end + end + + require "crawlscope" + load ARGV.fetch(0) + + configuration = Crawlscope.configuration + CrawlscopeConfiguration.apply + abort "configuration replaced" unless Crawlscope.configuration.equal?(configuration) + abort "base URL missing" unless configuration.base_url == "http://localhost:3000" + abort "sitemap missing" unless configuration.sitemap_path == "public/sitemap.xml" + RUBY + _stdout, stderr, status = Open3.capture3( + RbConfig.ruby, + "-I#{File.expand_path("../../lib", __dir__)}", + "-e", + script, + initializer + ) + assert status.success?, stderr + end + end +end