Skip to content

Commit

Permalink
Significant refactor of the Routes plugin (#882)
Browse files Browse the repository at this point in the history
* Significant refactor of the Routes plugin

- More modular and OOP
- Better Roda plugin layout
- Dynamic `index` route now supported instead of static index.html
- Downstream plugins can leverage file-based routing themselves

* Move monkey-patched Roda Public code into streamlined standalone SSG plugin

- Also serve static `index.html` using the same `Rack::Files` server

* # SSR mode and Fast Refresh mode are mutually exclusive

* add comment

* Simplify by moving more Roda-specific code into the server plugin
  • Loading branch information
jaredcwhite authored Apr 23, 2024
1 parent 1cdd32b commit 24b39fa
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 197 deletions.
2 changes: 2 additions & 0 deletions bridgetown-core/lib/bridgetown-core/concerns/site/ssr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def ssr?

def enable_ssr
Bridgetown.logger.info "SSR:", "enabled."
config.fast_refresh = false # SSR mode and Fast Refresh mode are mututally exclusive
@ssr_enabled = true
end

Expand All @@ -44,6 +45,7 @@ def ssr_setup(&block)
end

def ssr_first_read
# TODO: this shouldn't be running twice, right?!
Bridgetown::Hooks.trigger :site, :pre_read, self
defaults_reader.tap do |d|
d.path_defaults.clear
Expand Down
2 changes: 0 additions & 2 deletions bridgetown-core/lib/bridgetown-core/rack/boot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
require "zeitwerk"
require "roda"
require "json"
require "roda/plugins/public"

Bridgetown::Current.preloaded_configuration ||= Bridgetown.configuration

require_relative "logger"
require_relative "routes"
require_relative "static_indexes"

module Bridgetown
module Rack
Expand Down
29 changes: 2 additions & 27 deletions bridgetown-core/lib/bridgetown-core/rack/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,36 +112,11 @@ def merge(roda_app)
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
# Set up live reload if allowed, then run through all the Routes blocks.
#
# @param roda_app [Roda]
# @return [void]
def start!(roda_app)
if Bridgetown::Current.preloaded_configuration.base_path == "/"
load_all_routes roda_app
return
end

# Support custom base_path configurations
roda_app.request.on(
Bridgetown::Current.preloaded_configuration.base_path.delete_prefix("/")
) do
load_all_routes roda_app
end

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 [Roda]
# @return [void]
def load_all_routes(roda_app)
roda_app.request.public

def load_all(roda_app)
if Bridgetown.env.development? &&
!Bridgetown::Current.preloaded_configuration.skip_live_reload
setup_live_reload roda_app
Expand Down
31 changes: 0 additions & 31 deletions bridgetown-core/lib/bridgetown-core/rack/static_indexes.rb

This file was deleted.

52 changes: 45 additions & 7 deletions bridgetown-core/lib/roda/plugins/bridgetown_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def self.load_dependencies(app) # rubocop:disable Metrics
"plugin"
end

app.extend ClassMethods # we need to do this here because Roda hasn't done it yet
app.plugin :initializers
app.plugin :method_override
app.plugin :all_verbs
Expand All @@ -20,7 +21,7 @@ def self.load_dependencies(app) # rubocop:disable Metrics
app.plugin :json_parser
app.plugin :indifferent_params
app.plugin :cookies, path: "/"
app.plugin :public, root: Bridgetown::Current.preloaded_configuration.destination
app.plugin :ssg, root: Bridgetown::Current.preloaded_configuration.destination
app.plugin :not_found do
output_folder = Bridgetown::Current.preloaded_configuration.destination
File.read(File.join(output_folder, "404.html"))
Expand Down Expand Up @@ -51,6 +52,7 @@ def self.load_dependencies(app) # rubocop:disable Metrics
result.transform!.output
end

# TODO: there may be a better way to do this, see `exception_page_css` instance method
ExceptionPage.class_eval do # rubocop:disable Metrics/BlockLength
def self.css
<<~CSS
Expand Down Expand Up @@ -107,8 +109,16 @@ def self.css
CSS
end
end
end

module ClassMethods
def root_hook(&block)
opts[:root_hook] = block
end
end

app.before do
module InstanceMethods
def initialize_bridgetown_context
if self.class.opts[:bridgetown_site]
# The site had previously been initialized via the bridgetown_ssr plugin
Bridgetown::Current.sites[self.class.opts[:bridgetown_site].label] =
Expand All @@ -117,11 +127,22 @@ def self.css
end
Bridgetown::Current.preloaded_configuration ||=
self.class.opts[:bridgetown_preloaded_config]
end

def initialize_bridgetown_root # rubocop:todo Metrics/AbcSize
request.root do
output_folder = Bridgetown::Current.preloaded_configuration.destination
File.read(File.join(output_folder, "index.html"))
rescue StandardError
hook_result = instance_exec(&self.class.opts[:root_hook]) if self.class.opts[:root_hook]
next hook_result if hook_result

status, headers, body = self.class.opts[:ssg_server].serving(
request, File.join(self.class.opts[:ssg_root], "index.html")
)
response_headers = response.headers
response_headers.replace(headers)

request.halt [status, response_headers, body]
rescue StandardError => e
Bridgetown.logger.debug("Root handler error: #{e.message}")
response.status = 500
"<p>ERROR: cannot find <code>index.html</code> in the output folder.</p>"
end
Expand All @@ -138,9 +159,26 @@ def cookies
_previous_roda_cookies.with_indifferent_access
end

# Starts up the Bridgetown routing system
# Start up the Bridgetown routing system
def bridgetown
Bridgetown::Rack::Routes.start!(scope)
scope.initialize_bridgetown_context
scope.initialize_bridgetown_root

# Run the static file server
ssg

# There are two different code paths depending on if there's a site `base_path` configured
if Bridgetown::Current.preloaded_configuration.base_path == "/"
Bridgetown::Rack::Routes.load_all scope
return
end

# Support custom base_path configurations
on(Bridgetown::Current.preloaded_configuration.base_path.delete_prefix("/")) do
Bridgetown::Rack::Routes.load_all scope
end

nil
end
end
end
Expand Down
72 changes: 72 additions & 0 deletions bridgetown-core/lib/roda/plugins/ssg.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

require "uri"
require "rack/files"

class Roda
module RodaPlugins
# This is a simplifed and modified variant of Roda's Public core plugin. It adds additional
# functionality so that you can host an entire static site through Roda. What's necessary for
# this to work is handling "pretty" URLs, aka:
#
# /path/to/page -> /path/to/page.html or /path/to/page/index.html
# /path/to/page/ -> /path/to/page/index.html
#
# It does not support serving compressed files, as that should ideally be handled through a
# proxy or CDN layer in your architecture.
module SSG
PARSER = URI::DEFAULT_PARSER

def self.configure(app, opts = {})
app.opts[:ssg_root] = app.expand_path(opts.fetch(:root, "public"))
app.opts[:ssg_server] = Rack::Files.new(app.opts[:ssg_root])
end

module RequestMethods
def ssg
return unless is_get?

path = PARSER.unescape(real_remaining_path)
return if path.include?("\0")

server = roda_class.opts[:ssg_server]
path = File.join(server.root, *segments_for_path(path))

return unless File.file?(path)

status, headers, body = server.serving(self, path)
response_headers = response.headers
response_headers.replace(headers)
halt [status, response_headers, body]
end

# TODO: this could be refactored a bit
def segments_for_path(path) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
segments = []

path.split("/").each do |seg|
next if seg.empty? || seg == "."

seg == ".." ? segments.pop : segments << seg
end

path = File.join(roda_class.opts[:ssg_root], *segments)
unless File.file?(path)
path = File.join(path, "index.html")
if File.file?(path)
segments << "index.html"
else
segments[segments.size - 1] = "#{segments.last}.html"
end
end

segments
rescue IndexError
nil
end
end
end

register_plugin :ssg, SSG
end
end
1 change: 0 additions & 1 deletion bridgetown-core/lib/site_template/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ output

# Dependency folders
node_modules
vendor

# Caches
.sass-cache
Expand Down
Loading

0 comments on commit 24b39fa

Please sign in to comment.