From 8b0a13fab7b546371c08e260c36dc9c53e3304cd Mon Sep 17 00:00:00 2001 From: Jared White Date: Sun, 15 May 2022 11:11:25 -0700 Subject: [PATCH] Allow route classes to be prioritized to adjust run order (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow route classes to be prioritized to adjust run order * Add Roda/SSR integration testing — more to come! * improve route testing * fix cops * leave the plugins folder in place * fix: plugins & components folders can now be absent * Add site `server_shutdown` hook via Puma * Move routes prioritization to a concern, better documentation * Add prioritiziation feature to Builders * Add documentation for the later value --- Gemfile | 1 + bridgetown-builder/lib/bridgetown-builder.rb | 2 +- .../lib/bridgetown-builder/plugin.rb | 10 +++ bridgetown-builder/test/test_generators.rb | 15 +++- bridgetown-core/lib/bridgetown-core.rb | 2 + .../lib/bridgetown-core/commands/start.rb | 3 + .../bridgetown-core/concerns/prioritizable.rb | 44 ++++++++++++ .../bridgetown-core/frontmatter_defaults.rb | 6 +- bridgetown-core/lib/bridgetown-core/plugin.rb | 41 ++--------- .../lib/bridgetown-core/rack/boot.rb | 5 ++ .../lib/bridgetown-core/rack/routes.rb | 69 ++++++++++++++++++- .../lib/bridgetown-core/watcher.rb | 7 +- bridgetown-core/test/ssr/config.ru | 13 ++++ bridgetown-core/test/ssr/server/roda_app.rb | 12 ++++ .../test/ssr/server/routes/hello.rb | 14 ++++ .../test/ssr/server/routes/kello.rb | 12 ++++ bridgetown-core/test/ssr/src/index.md | 1 + bridgetown-core/test/ssr/test_output/404.html | 1 + .../test/ssr/test_output/index.html | 1 + bridgetown-core/test/test_ssr.rb | 42 +++++++++++ bridgetown-website/src/_docs/plugins.md | 28 +++++--- bridgetown-website/src/_docs/routes.md | 29 ++++++++ 22 files changed, 302 insertions(+), 56 deletions(-) create mode 100644 bridgetown-core/lib/bridgetown-core/concerns/prioritizable.rb create mode 100644 bridgetown-core/test/ssr/config.ru create mode 100644 bridgetown-core/test/ssr/server/roda_app.rb create mode 100644 bridgetown-core/test/ssr/server/routes/hello.rb create mode 100644 bridgetown-core/test/ssr/server/routes/kello.rb create mode 100644 bridgetown-core/test/ssr/src/index.md create mode 100644 bridgetown-core/test/ssr/test_output/404.html create mode 100644 bridgetown-core/test/ssr/test_output/index.html create mode 100644 bridgetown-core/test/test_ssr.rb diff --git a/Gemfile b/Gemfile index 3daa0f16e..f8f0dd265 100644 --- a/Gemfile +++ b/Gemfile @@ -16,6 +16,7 @@ group :test do gem "minitest-profile" gem "minitest-reporters" gem "nokogiri", "~> 1.7" + gem "rack-test" gem "rspec" gem "rspec-mocks" gem "rubocop-bridgetown", "~> 0.3.0", require: false diff --git a/bridgetown-builder/lib/bridgetown-builder.rb b/bridgetown-builder/lib/bridgetown-builder.rb index b8dab37e8..894bbd599 100644 --- a/bridgetown-builder/lib/bridgetown-builder.rb +++ b/bridgetown-builder/lib/bridgetown-builder.rb @@ -15,7 +15,7 @@ module Builders # SiteBuilder is the superclass sites can subclass to create any number of # builders, but if the site hasn't defined it explicitly, this is a no-op if defined?(SiteBuilder) - SiteBuilder.descendants.map do |c| + SiteBuilder.descendants.sort.map do |c| c.new(c.name, site).build_with_callbacks end end diff --git a/bridgetown-builder/lib/bridgetown-builder/plugin.rb b/bridgetown-builder/lib/bridgetown-builder/plugin.rb index d3c23d504..2714f7352 100644 --- a/bridgetown-builder/lib/bridgetown-builder/plugin.rb +++ b/bridgetown-builder/lib/bridgetown-builder/plugin.rb @@ -9,6 +9,16 @@ module Bridgetown module Builders class PluginBuilder + include Bridgetown::Prioritizable + + self.priorities = { + highest: 100, + high: 10, + normal: 0, + low: -10, + lowest: -100, + }.freeze + include DSL::Generators include DSL::Helpers include DSL::Hooks diff --git a/bridgetown-builder/test/test_generators.rb b/bridgetown-builder/test/test_generators.rb index f03416dc5..8ea8cbcaf 100644 --- a/bridgetown-builder/test/test_generators.rb +++ b/bridgetown-builder/test/test_generators.rb @@ -3,6 +3,8 @@ require "helper" class GeneratorBuilder < Builder + priority :low + def build generator do site.data[:site_metadata][:title] = "Test Title" @@ -10,12 +12,23 @@ def build end end +class GeneratorBuilder2 < Builder + def build + generator do + site.data[:site_metadata][:title] = "Test Title 2" + end + end +end + class TestGenerators < BridgetownUnitTest context "creating a generator" do setup do Bridgetown.sites.clear @site = Site.new(site_configuration) - @builder = GeneratorBuilder.new("Generator Test", @site).build_with_callbacks + @builders = [GeneratorBuilder, GeneratorBuilder2].sort + @builders.each_with_index do |builder, index| + builder.new("Generator Test #{index}", @site).build_with_callbacks + end end should "be loaded on site setup" do diff --git a/bridgetown-core/lib/bridgetown-core.rb b/bridgetown-core/lib/bridgetown-core.rb index e5c5f6530..441f1723b 100644 --- a/bridgetown-core/lib/bridgetown-core.rb +++ b/bridgetown-core/lib/bridgetown-core.rb @@ -32,6 +32,7 @@ def require_all(path) # 3rd party require "active_support" +require "active_support/core_ext/class/attribute" require "active_support/core_ext/hash/keys" require "active_support/core_ext/module/delegation" require "active_support/core_ext/object/blank" @@ -90,6 +91,7 @@ module Bridgetown autoload :LogAdapter, "bridgetown-core/log_adapter" autoload :PluginContentReader, "bridgetown-core/readers/plugin_content_reader" autoload :PluginManager, "bridgetown-core/plugin_manager" + autoload :Prioritizable, "bridgetown-core/concerns/prioritizable" autoload :Publishable, "bridgetown-core/concerns/publishable" autoload :Publisher, "bridgetown-core/publisher" autoload :Reader, "bridgetown-core/reader" diff --git a/bridgetown-core/lib/bridgetown-core/commands/start.rb b/bridgetown-core/lib/bridgetown-core/commands/start.rb index dfdb92321..13a3eb767 100644 --- a/bridgetown-core/lib/bridgetown-core/commands/start.rb +++ b/bridgetown-core/lib/bridgetown-core/commands/start.rb @@ -70,6 +70,9 @@ def output_header(mode) end cli = Puma::CLI.new puma_args + cli.launcher.events.on_stopped do + Bridgetown::Hooks.trigger :site, :server_shutdown + end cli.run end diff --git a/bridgetown-core/lib/bridgetown-core/concerns/prioritizable.rb b/bridgetown-core/lib/bridgetown-core/concerns/prioritizable.rb new file mode 100644 index 000000000..e2f71107f --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/concerns/prioritizable.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module Bridgetown + module Prioritizable + module ClassMethods + # @!method priorities + # @return [Hash] + + # Get or set the priority of this class. When called without an + # argument it returns the priority. When an argument is given, it will + # set the priority. + # + # @param priority [Symbol] new priority (optional) + # Valid options are: `:lowest`, `:low`, `:normal`, `:high`, `:highest` + # @return [Symbol] + def priority(priority = nil) + @priority ||= nil + @priority = priority if priority && priorities.key?(priority) + @priority || :normal + end + + # Spaceship is priority [higher -> lower] + # + # @param other [Class] The class to be compared. + # @return [Integer] -1, 0, 1. + def <=>(other) + priorities[other.priority] <=> priorities[priority] + end + end + + def self.included(klass) + klass.extend ClassMethods + klass.class_attribute :priorities, instance_accessor: false + end + + # Spaceship is priority [higher -> lower] + # + # @param other [object] The object to be compared. + # @return [Integer] -1, 0, 1. + def <=>(other) + self.class <=> other.class + end + end +end diff --git a/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb b/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb index 390c98d55..26a3842d1 100644 --- a/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb +++ b/bridgetown-core/lib/bridgetown-core/frontmatter_defaults.rb @@ -31,7 +31,7 @@ def ensure_time!(set) # @param path [String] the relative path of the resource # @param collection [Symbol] :posts, :pages, etc. # - # @returns [Hash] all default values (an empty hash if there are none) + # @return [Hash] all default values (an empty hash if there are none) def all(path, collection) defaults = {} @@ -123,7 +123,7 @@ def strip_collections_dir(path) # @param scope [Hash] the defaults set being asked about # @param collection [Symbol] the collection of the resource being processed # - # @returns [Boolean] whether either of the above conditions are satisfied + # @return [Boolean] whether either of the above conditions are satisfied def applies_collection?(scope, collection) !scope.key?("collection") || scope["collection"].eql?(collection.to_s) end @@ -132,7 +132,7 @@ def applies_collection?(scope, collection) # # @param set [Hash] the default value hash as defined in bridgetown.config.yml # - # @returns [Boolean] if the set is valid and can be used + # @return [Boolean] if the set is valid and can be used def valid?(set) set.is_a?(Hash) && set["values"].is_a?(Hash) end diff --git a/bridgetown-core/lib/bridgetown-core/plugin.rb b/bridgetown-core/lib/bridgetown-core/plugin.rb index fcf16399c..35dba63bc 100644 --- a/bridgetown-core/lib/bridgetown-core/plugin.rb +++ b/bridgetown-core/lib/bridgetown-core/plugin.rb @@ -3,49 +3,18 @@ module Bridgetown class Plugin extend ActiveSupport::DescendantsTracker + include Bridgetown::Prioritizable - PRIORITIES = { - low: -10, + self.priorities = { highest: 100, - lowest: -100, - normal: 0, high: 10, + normal: 0, + low: -10, + lowest: -100, }.freeze SourceManifest = Struct.new(:origin, :components, :content, :layouts, keyword_init: true) - # Get or set the priority of this plugin. When called without an - # argument it returns the priority. When an argument is given, it will - # set the priority. - # - # priority - The Symbol priority (default: nil). Valid options are: - # :lowest, :low, :normal, :high, :highest - # - # Returns the Symbol priority. - def self.priority(priority = nil) - @priority ||= nil - @priority = priority if priority && PRIORITIES.key?(priority) - @priority || :normal - end - - # Spaceship is priority [higher -> lower] - # - # other - The class to be compared. - # - # Returns -1, 0, 1. - def self.<=>(other) - PRIORITIES[other.priority] <=> PRIORITIES[priority] - end - - # Spaceship is priority [higher -> lower] - # - # other - The class to be compared. - # - # Returns -1, 0, 1. - def <=>(other) - self.class <=> other.class - end - # Initialize a new plugin. This should be overridden by the subclass. # # config - The Hash of configuration options. diff --git a/bridgetown-core/lib/bridgetown-core/rack/boot.rb b/bridgetown-core/lib/bridgetown-core/rack/boot.rb index eb1ad6b9c..dfc16aa1d 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/boot.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/boot.rb @@ -19,6 +19,11 @@ class << self attr_accessor :loaders_manager end + # Start up the Roda Rack application and the Zeitwerk autoloaders. Ensure the + # Roda app is provided the preloaded Bridgetown site configuration. Handle + # any uncaught Roda errors. + # + # @param [Bridgetown::Rack::Roda] optional, defaults to the `RodaApp` constant def self.boot(roda_app = nil) self.loaders_manager = Bridgetown::Utils::LoadersManager.new(Bridgetown::Current.preloaded_configuration) diff --git a/bridgetown-core/lib/bridgetown-core/rack/routes.rb b/bridgetown-core/lib/bridgetown-core/rack/routes.rb index d8a1c137e..3ac7a9bd9 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/routes.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/routes.rb @@ -9,19 +9,49 @@ class << self end class Routes + include Bridgetown::Prioritizable + + self.priorities = { + highest: "010", + high: "020", + normal: "030", + low: "040", + lowest: "050", + }.freeze + class << self - attr_accessor :tracked_subclasses, :router_block + # @return [Hash] + attr_accessor :tracked_subclasses + + # @return [Proc] + attr_accessor :router_block + + # Spaceship is priority [higher -> lower] + # + # @param other [Class(Routes)] The class to be compared. + # @return [Integer] -1, 0, 1. + def <=>(other) + "#{priorities[priority]}#{self}" <=> "#{priorities[other.priority]}#{other}" + end + # @param base [Class(Routes)] def inherited(base) Bridgetown::Rack::Routes.track_subclass base super end + # @param klass [Class(Routes)] def track_subclass(klass) Bridgetown::Rack::Routes.tracked_subclasses ||= {} Bridgetown::Rack::Routes.tracked_subclasses[klass.name] = klass end + # @return [Array] + def sorted_subclasses + Bridgetown::Rack::Routes.tracked_subclasses&.values&.sort + end + + # @return [void] def reload_subclasses Bridgetown::Rack::Routes.tracked_subclasses&.each_key do |klassname| Kernel.const_get(klassname) @@ -30,16 +60,38 @@ def reload_subclasses end end + # Add a router block via the current Routes class + # + # Example: + # + # class Routes::Hello < Bridgetown::Rack::Routes + # route do |r| + # r.get "hello", String do |name| + # { hello: "friend #{name}" } + # end + # end + # end + # + # @param block [Proc] def route(&block) self.router_block = block end + # Initialize a new Routes instance and execute the route as part of the + # Roda app request cycle + # + # @param roda_app [Bridgetown::Rack::Roda] def merge(roda_app) return unless router_block new(roda_app).handle_routes end + # Start the Roda app request cycle. There are two different code paths + # depending on if there's a site `base_path` configured + # + # @param roda_app [Bridgetown::Rack::Roda] + # @return [void] def start!(roda_app) if Bridgetown::Current.preloaded_configuration.base_path == "/" load_all_routes roda_app @@ -56,6 +108,12 @@ def start!(roda_app) nil end + # Run the Roda public plugin first, set up live reload if allowed, then + # run through all the Routes blocks. If the file-based router plugin + # is available, kick off that request process next. + # + # @param roda_app [Bridgetown::Rack::Roda] + # @return [void] def load_all_routes(roda_app) roda_app.request.public @@ -64,7 +122,7 @@ def load_all_routes(roda_app) setup_live_reload roda_app end - Bridgetown::Rack::Routes.tracked_subclasses&.each_value do |klass| + Bridgetown::Rack::Routes.sorted_subclasses&.each do |klass| klass.merge roda_app end @@ -73,6 +131,7 @@ def load_all_routes(roda_app) Bridgetown::Routes::RodaRouter.start!(roda_app) end + # @param app [Bridgetown::Rack::Roda] def setup_live_reload(app) # rubocop:disable Metrics/AbcSize sleep_interval = 0.2 file_to_check = File.join(app.class.opts[:bridgetown_preloaded_config].destination, @@ -102,14 +161,20 @@ def setup_live_reload(app) # rubocop:disable Metrics/AbcSize end end + # @param roda_app [Bridgetown::Rack::Roda] def initialize(roda_app) @_roda_app = roda_app end + # Execute the router block via the instance, passing it the Roda request + # + # @return [Object] whatever is returned by the router block as expected + # by the Roda API def handle_routes instance_exec(@_roda_app.request, &self.class.router_block) end + # Any missing methods are passed along to the underlying Roda app if possible def method_missing(method_name, *args, **kwargs, &block) if @_roda_app.respond_to?(method_name.to_sym) @_roda_app.send method_name.to_sym, *args, **kwargs, &block diff --git a/bridgetown-core/lib/bridgetown-core/watcher.rb b/bridgetown-core/lib/bridgetown-core/watcher.rb index 98a59d5f2..0843eb6e9 100644 --- a/bridgetown-core/lib/bridgetown-core/watcher.rb +++ b/bridgetown-core/lib/bridgetown-core/watcher.rb @@ -42,10 +42,9 @@ def watch(site, options, &block) # # @param (see #watch) def load_paths_to_watch(site, options) - site.plugin_manager.plugins_path.select { |path| Dir.exist?(path) } - .then do |paths| - (paths + options.autoload_paths).uniq - end + (site.plugin_manager.plugins_path + options.autoload_paths).uniq.select do |path| + Dir.exist?(path) + end end # Start a listener to watch for changes and call {#reload_site} diff --git a/bridgetown-core/test/ssr/config.ru b/bridgetown-core/test/ssr/config.ru new file mode 100644 index 000000000..10007443e --- /dev/null +++ b/bridgetown-core/test/ssr/config.ru @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "bridgetown-core/rack/boot" + +Bridgetown::Current.preloaded_configuration = Bridgetown::Configuration.from( + root_dir: __dir__, + source: File.join(__dir__, "src"), + destination: File.join(__dir__, "test_output") +) + +Bridgetown::Rack.boot + +run RodaApp.freeze.app # see server/roda_app.rb diff --git a/bridgetown-core/test/ssr/server/roda_app.rb b/bridgetown-core/test/ssr/server/roda_app.rb new file mode 100644 index 000000000..7c7570f66 --- /dev/null +++ b/bridgetown-core/test/ssr/server/roda_app.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class RodaApp < Bridgetown::Rack::Roda + plugin :bridgetown_ssr do |site| + site.data.iterations ||= 0 + site.data.iterations += 1 + end + + route do |_r| + Bridgetown::Rack::Routes.start! self + end +end diff --git a/bridgetown-core/test/ssr/server/routes/hello.rb b/bridgetown-core/test/ssr/server/routes/hello.rb new file mode 100644 index 000000000..1011b83cc --- /dev/null +++ b/bridgetown-core/test/ssr/server/routes/hello.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Routes::Hello < Bridgetown::Rack::Routes + priority :lowest + + route do |r| + saved_value = bridgetown_site.data.save_value + + # route: GET /hello/:name + r.get "hello", String do |name| + { hello: "friend #{name} #{saved_value}" } + end + end +end diff --git a/bridgetown-core/test/ssr/server/routes/kello.rb b/bridgetown-core/test/ssr/server/routes/kello.rb new file mode 100644 index 000000000..94ccd4fb5 --- /dev/null +++ b/bridgetown-core/test/ssr/server/routes/kello.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Routes::Kello < Bridgetown::Rack::Routes + route do |r| + bridgetown_site.data.save_value = "VALUE" + + # route: GET /hello/:name + r.get "kello", String do |name| + { kello: "kriend #{name}" } + end + end +end diff --git a/bridgetown-core/test/ssr/src/index.md b/bridgetown-core/test/ssr/src/index.md new file mode 100644 index 000000000..0317634c8 --- /dev/null +++ b/bridgetown-core/test/ssr/src/index.md @@ -0,0 +1 @@ +Hello **world**! \ No newline at end of file diff --git a/bridgetown-core/test/ssr/test_output/404.html b/bridgetown-core/test/ssr/test_output/404.html new file mode 100644 index 000000000..79c4fa512 --- /dev/null +++ b/bridgetown-core/test/ssr/test_output/404.html @@ -0,0 +1 @@ +

404

\ No newline at end of file diff --git a/bridgetown-core/test/ssr/test_output/index.html b/bridgetown-core/test/ssr/test_output/index.html new file mode 100644 index 000000000..a9ca1f0fb --- /dev/null +++ b/bridgetown-core/test/ssr/test_output/index.html @@ -0,0 +1 @@ +

Index

\ No newline at end of file diff --git a/bridgetown-core/test/test_ssr.rb b/bridgetown-core/test/test_ssr.rb new file mode 100644 index 000000000..502eff9f5 --- /dev/null +++ b/bridgetown-core/test/test_ssr.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "helper" + +class TestSSR < BridgetownUnitTest + include Rack::Test::Methods + + def app + @@ssr_app ||= Rack::Builder.parse_file(File.expand_path("ssr/config.ru", __dir__)).first # rubocop:disable Style/ClassVars + end + + context "Roda-powered Bridgetown server" do + setup do + Bridgetown::Current.site = nil + end + + teardown do + Bridgetown.sites.clear + Bridgetown::Current.preloaded_configuration = nil + end + + should "return the index page" do + get "/" + assert last_response.ok? + assert_equal "

Index

", last_response.body + end + + should "return JSON for the hello route" do + get "/hello/world" + assert last_response.ok? + assert_equal({ hello: "friend world VALUE" }.to_json, last_response.body) + end + + should "preserve site data between live reloads" do + app # ensure it's been run + site = @@ssr_app.opts[:bridgetown_site] + assert_equal 1, site.data.iterations + site.reset(soft: true) + assert_equal 2, site.data.iterations + end + end +end diff --git a/bridgetown-website/src/_docs/plugins.md b/bridgetown-website/src/_docs/plugins.md index aab874ece..2dee239c7 100644 --- a/bridgetown-website/src/_docs/plugins.md +++ b/bridgetown-website/src/_docs/plugins.md @@ -185,19 +185,29 @@ Converters change a markup language from one format to another. #### Priority Flag -You can configure a Legacy API plugin (mainly generators and converters) with a specific `priority` flag. This flag determines what order the plugin is loaded in. +You can configure a plugin (builders, converters, etc.) with a specific `priority` flag. This flag determines what order the plugin is loaded in. -Valid values are: :lowest, :low, :normal, - :high, and :highest. Highest priority - matches are applied first, lowest priority are applied last. +The default priority is `:normal`. Valid values are: -Here is how you’d specify this flag: +:lowest, :low, :normal, :high, and :highest. +Highest priority plugins are run first, lowest priority are run last. + +Examples of specifying this flag: ```ruby -module MySite - class UpcaseConverter < Converter - priority :low - ... +class Builders::DoImportantStuff < SiteBuilder + priority :highest + + def build + # do really important stuff here + end +end + +class Builders::CanWaitUntilLater < SiteBuilder + priority :low + + def build + # stuff that'll get run later (after the really important stuff) end end ``` diff --git a/bridgetown-website/src/_docs/routes.md b/bridgetown-website/src/_docs/routes.md index aa442e919..0e50e557e 100644 --- a/bridgetown-website/src/_docs/routes.md +++ b/bridgetown-website/src/_docs/routes.md @@ -62,6 +62,35 @@ class RodaApp < Bridgetown::Rack::Roda end ``` +### Priority Flag + +You can configure a `Routes` class with a specific `priority` flag. This flag determines what order the router is loaded in relative to other routers. + +The default priority is `:normal`. Valid values are: + +:lowest, :low, :normal, :high, and :highest. +Highest priority plugins are run first, lowest priority are run last. + +Examples of specifying this flag: + +```ruby +class Routes::InitialSetup < Bridgetown::Rack::Routes + priority :highest + + route do |r| + r.session[:adding_this] ||= "value" + end +end + +class Routes::LaterOn < Bridgetown::Rack::Routes + route do |r| + r.get "later" do + { session_value: r.session[:adding_this] } # :session_value => "value" + end + end +end +``` + ## File-based Dynamic Routes **But wait, there’s more!** We now ship a new gem you can opt-into (as part of the Bridgetown monorepo) called `bridgetown-routes`. Within minutes of installing it, you gain the ability to write file-based dynamic routes with view templates right inside your source folder!