diff --git a/CHANGELOG.md b/CHANGELOG.md index f4cd36d11..7c280362f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix: Lock Liquid to version < 5.5 (unresolved data leakage and generation errors otherwise) - Fix: esbuild file endings from previous release +- Use bundler binstubs to create `bin/bt` shortcut instead of by `cp` ## [1.3.3] - 2024-03-16 diff --git a/Gemfile.lock b/Gemfile.lock index 9ced064d4..fd02a5a03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -23,6 +23,7 @@ PATH liquid (>= 5.0, < 5.5) listen (~> 3.0) rack (>= 3.0) + rackup (~> 2.0) rake (>= 13.0) roda (~> 3.46) rouge (>= 3.0, < 5.0) @@ -129,10 +130,10 @@ GEM racc (~> 1.4) nokogiri (1.16.4-x86_64-linux) racc (~> 1.4) - nokolexbor (0.5.3) - nokolexbor (0.5.3-arm64-darwin) - nokolexbor (0.5.3-x86_64-darwin) - nokolexbor (0.5.3-x86_64-linux) + nokolexbor (0.5.4) + nokolexbor (0.5.4-arm64-darwin) + nokolexbor (0.5.4-x86_64-darwin) + nokolexbor (0.5.4-x86_64-linux) parallel (1.24.0) parser (3.3.0.5) ast (~> 2.4.1) @@ -142,6 +143,9 @@ GEM rack (3.0.10) rack-test (2.1.0) rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) rainbow (3.1.1) rake (13.2.1) rb-fsevent (0.11.2) @@ -223,6 +227,7 @@ GEM concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) uri (0.13.0) + webrick (1.8.1) yard (0.9.36) zeitwerk (2.6.13) diff --git a/bridgetown-core/bin/bt b/bridgetown-core/bin/bt new file mode 100755 index 000000000..507ff72fe --- /dev/null +++ b/bridgetown-core/bin/bt @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# `bt` is a shortcut to `bridgetown` + +load File.join(File.dirname(__FILE__), 'bridgetown') diff --git a/bridgetown-core/bridgetown-core.gemspec b/bridgetown-core/bridgetown-core.gemspec index 538461ad6..e26b04f81 100644 --- a/bridgetown-core/bridgetown-core.gemspec +++ b/bridgetown-core/bridgetown-core.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.files = `git ls-files -z`.split("\x0").reject do |f| f.match(%r!^(benchmark|features|script|test)/!) end - s.executables = ["bridgetown"] + s.executables = ["bridgetown", "bt"] # `bt` is a shortcut to `bridgetown` command s.bindir = "bin" s.require_paths = ["lib"] @@ -46,6 +46,7 @@ Gem::Specification.new do |s| s.add_runtime_dependency("liquid", [">= 5.0", "< 5.5"]) s.add_runtime_dependency("listen", "~> 3.0") s.add_runtime_dependency("rack", ">= 3.0") + s.add_runtime_dependency("rackup", "~> 2.0") s.add_runtime_dependency("rake", ">= 13.0") s.add_runtime_dependency("roda", "~> 3.46") s.add_runtime_dependency("rouge", [">= 3.0", "< 5.0"]) diff --git a/bridgetown-core/lib/bridgetown-core.rb b/bridgetown-core/lib/bridgetown-core.rb index bc362b85e..fd62d5eb7 100644 --- a/bridgetown-core/lib/bridgetown-core.rb +++ b/bridgetown-core/lib/bridgetown-core.rb @@ -101,7 +101,6 @@ module Bridgetown autoload :Slot, "bridgetown-core/slot" autoload :StaticFile, "bridgetown-core/static_file" autoload :Transformable, "bridgetown-core/concerns/transformable" - autoload :URL, "bridgetown-core/url" autoload :Utils, "bridgetown-core/utils" autoload :VERSION, "bridgetown-core/version" autoload :Watcher, "bridgetown-core/watcher" @@ -375,9 +374,7 @@ def build_errors_path ) end end -end -module Bridgetown module Model; end module Resource @@ -390,6 +387,13 @@ def self.register_extension(mod) end end end + + # mixin for identity so Roda knows to call renderable objects + module RodaCallable + def self.===(other) + other.class < self + end + end end Zeitwerk.with_loader do |l| diff --git a/bridgetown-core/lib/bridgetown-core/commands/build.rb b/bridgetown-core/lib/bridgetown-core/commands/build.rb index c09a69648..ae4d2047a 100644 --- a/bridgetown-core/lib/bridgetown-core/commands/build.rb +++ b/bridgetown-core/lib/bridgetown-core/commands/build.rb @@ -31,23 +31,22 @@ def self.print_startup_message def build Bridgetown.logger.adjust_verbosity(options) - unless caller_locations.find do |loc| - loc.to_s.include?("bridgetown-core/commands/start.rb") - end - self.class.print_startup_message - end - # @type [Bridgetown::Configuration] config_options = configuration_with_overrides( options, Bridgetown::Current.preloaded_configuration ) - config_options.run_initializers! context: :static + self.class.print_startup_message unless config_options["start_command"] - config_options["serving"] = false unless config_options["serving"] + Process.setproctitle( + "bridgetown #{Bridgetown::VERSION} " \ + "(#{config_options["start_command"] ? "start" : "build"}) [#{File.basename(Dir.pwd)}]" + ) + + config_options.run_initializers! context: :static if !Bridgetown.env.production? && - !config_options[:skip_frontend] && config_options["using_puma"] + !config_options[:skip_frontend] && config_options["start_command"] if Bridgetown::Utils.frontend_bundler_type(config_options[:root_dir]) == :esbuild Bridgetown::Utils.update_esbuild_autogenerated_config config_options end @@ -96,19 +95,6 @@ def build_site(config_options) @site.process Bridgetown.logger.info "Done! đ", "#{"Completed".bold.green} in less than " \ "#{(Time.now - t).ceil(2)} seconds." - - return unless config_options[:using_puma] - - require "socket" - external_ip = Socket.ip_address_list.find do |ai| - ai.ipv4? && !ai.ipv4_loopback? - end&.ip_address - scheme = config_options.bind&.split("://")&.first == "ssl" ? "https" : "http" - port = config_options.bind&.split(":")&.last || ENV["BRIDGETOWN_PORT"] || 4000 - Bridgetown.logger.info "" - Bridgetown.logger.info "Now serving at:", "#{scheme}://localhost:#{port}".magenta - Bridgetown.logger.info "", "#{scheme}://#{external_ip}:#{port}".magenta if external_ip - Bridgetown.logger.info "" end # Watch for file changes and rebuild the site. diff --git a/bridgetown-core/lib/bridgetown-core/commands/new.rb b/bridgetown-core/lib/bridgetown-core/commands/new.rb index 2994f910b..70cbe317e 100644 --- a/bridgetown-core/lib/bridgetown-core/commands/new.rb +++ b/bridgetown-core/lib/bridgetown-core/commands/new.rb @@ -224,8 +224,8 @@ def bundle_install(path) Bridgetown.with_unbundled_env do inside(path) do run "bundle install", abort_on_failure: true + # create binstubs to `bin/bridgetown` and `bin/bt` run "bundle binstubs bridgetown-core" - run "cp bin/bridgetown bin/bt" end end end diff --git a/bridgetown-core/lib/bridgetown-core/commands/start.rb b/bridgetown-core/lib/bridgetown-core/commands/start.rb index 6d0ca3374..dca2a7590 100644 --- a/bridgetown-core/lib/bridgetown-core/commands/start.rb +++ b/bridgetown-core/lib/bridgetown-core/commands/start.rb @@ -1,103 +1,118 @@ # frozen_string_literal: true +require "rackup/server" + module Bridgetown + class Server < Rackup::Server + def start(after_stop_callback = nil) + trap(:INT) { exit } + super() + ensure + after_stop_callback&.call + end + + def name + server.to_s.split("::").last + end + + def using_puma? + name == "Puma" + end + + def serveable? + server + true + rescue LoadError, NameError + false + end + end + module Commands class Start < Thor::Group extend BuildOptions extend Summarizable include ConfigurationOverridable + include Bridgetown::Utils::PidTracker Registrations.register do register(Start, "start", "start", Start.summary) register(Start, "dev", "dev", "Alias of start") end - class_option :bind, aliases: "-B", desc: "URI for Puma to bind to (start with tcp://)" + class_option :port, + aliases: "-P", + type: :numeric, + default: 4000, + desc: "Serve your site on the specified port. Defaults to 4000." + class_option :bind, + aliases: "-B", + type: :string, + default: "0.0.0.0", + desc: "URL for the server to bind to." class_option :skip_frontend, type: :boolean, - desc: "Don't load the frontend bundler (always true for production)" + desc: "Don't load the frontend bundler (always true for production)." class_option :skip_live_reload, type: :boolean, - desc: "Don't use the live reload functionality (always true for production)" + desc: "Don't use the live reload functionality (always true for production)." def self.banner "bridgetown start [options]" end - summary "Start the Puma server, frontend bundler, and Bridgetown watcher" + summary "Start the web server, frontend bundler, and Bridgetown watcher" - def start # rubocop:todo Metrics/PerceivedComplexity + def start Bridgetown.logger.writer.enable_prefix Bridgetown::Commands::Build.print_startup_message sleep 0.25 - begin - require("puma/detect") - rescue LoadError - raise "** Puma server gem not found. Check your Gemfile and Bundler env? **" - end - options = Thor::CoreExt::HashWithIndifferentAccess.new(self.options) - options[:using_puma] = true + options[:start_command] = true # Load Bridgetown configuration into thread memory bt_options = configuration_with_overrides(options) + port = ENV.fetch("BRIDGETOWN_PORT", bt_options.port) + # TODO: support Puma serving HTTPS directly? + bt_bound_url = "http://#{bt_options.bind}:#{port}" # Set a local site URL in the config if one is not available - if Bridgetown.env.development? && !options["url"] - scheme = bt_options.bind&.split("://")&.first == "ssl" ? "https" : "http" - port = bt_options.bind&.split(":")&.last || ENV["BRIDGETOWN_PORT"] || 4000 - bt_options.url = "#{scheme}://localhost:#{port}" - end + bt_options.url = bt_bound_url if Bridgetown.env.development? && !options["url"] - puma_pid = - Process.fork do - require "puma/cli" - - Puma::Runner.class_eval do - def output_header(mode) - log "* Puma version: #{Puma::Const::PUMA_VERSION} (#{ruby_engine}) (\"#{Puma::Const::CODE_NAME}\")" # rubocop:disable Layout/LineLength - if mode == "cluster" - log "* Cluster Master PID: #{Process.pid}" - else - log "* PID: #{Process.pid}" - end - end - end + Bridgetown::Server.new({ + Host: bt_options.bind, + Port: port, + config: "config.ru", + }).tap do |server| + if server.serveable? + create_pid_dir - puma_args = [] - if bt_options[:bind] - puma_args << "--bind" - puma_args << bt_options[:bind] - end + bt_options.skip_live_reload = !server.using_puma? - cli = Puma::CLI.new puma_args - cli.launcher.events.on_stopped do + build_args = ["-w"] + ARGV.reject { |arg| arg == "start" } + build_pid = Process.fork { Bridgetown::Commands::Build.start(build_args) } + add_pid(build_pid, file: :bridgetown) + + after_stop_callback = -> { + say "Stopping Bridgetown server..." Bridgetown::Hooks.trigger :site, :server_shutdown - end - cli.run - end + Process.kill "SIGINT", build_pid + remove_pidfile :bridgetown - begin - Signal.trap("TERM") do - Process.kill "SIGINT", puma_pid - sleep 0.5 # let it breathe - exit 0 # this runs the ensure block below - end + # Shut down the frontend bundler etc. if they're running + unless Bridgetown.env.production? || bt_options[:skip_frontend] + Bridgetown::Utils::Aux.kill_processes + end + } - Process.setproctitle("bridgetown #{Bridgetown::VERSION} [#{File.basename(Dir.pwd)}]") - - build_args = ["-w"] + ARGV.reject { |arg| arg == "start" } - Bridgetown::Commands::Build.start(build_args) - rescue StandardError => e - Process.kill "SIGINT", puma_pid - sleep 0.5 - raise e - ensure - # Shut down esbuild, etc. if they're running - Bridgetown::Utils::Aux.kill_processes - end + Bridgetown.logger.info "" + Bridgetown.logger.info "Booting #{server.name} at:", bt_bound_url.to_s.magenta + Bridgetown.logger.info "" - sleep 0.5 # finish cleaning up + server.start(after_stop_callback) + else + say "Unable to find a Rack server." + end + end end end end diff --git a/bridgetown-core/lib/bridgetown-core/component.rb b/bridgetown-core/lib/bridgetown-core/component.rb index b4dc59ffb..ce8ae0ed5 100644 --- a/bridgetown-core/lib/bridgetown-core/component.rb +++ b/bridgetown-core/lib/bridgetown-core/component.rb @@ -202,7 +202,7 @@ def render_in(view_context, &block) # Subclasses can override this method to return a string from their own # template handling. def template - call || _renderer.render(self) + (method(:call).arity.zero? ? call : nil) || _renderer.render(self) end # Typically not used but here as a compatibility nod toward ViewComponent. diff --git a/bridgetown-core/lib/bridgetown-core/concerns/site/ssr.rb b/bridgetown-core/lib/bridgetown-core/concerns/site/ssr.rb index 4567dc925..d853b4d97 100644 --- a/bridgetown-core/lib/bridgetown-core/concerns/site/ssr.rb +++ b/bridgetown-core/lib/bridgetown-core/concerns/site/ssr.rb @@ -28,12 +28,11 @@ 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 def ssr_setup(&block) - config.serving = true - Bridgetown::Hooks.trigger :site, :pre_read, self ssr_first_read Bridgetown::Hooks.trigger :site, :post_read, self @@ -46,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 diff --git a/bridgetown-core/lib/bridgetown-core/configuration.rb b/bridgetown-core/lib/bridgetown-core/configuration.rb index 64a39da8d..f809af6ef 100644 --- a/bridgetown-core/lib/bridgetown-core/configuration.rb +++ b/bridgetown-core/lib/bridgetown-core/configuration.rb @@ -28,69 +28,70 @@ def initialize(*) # Strings rather than symbols are used for compatibility with YAML. DEFAULTS = { # Where things are - "root_dir" => Dir.pwd, - "plugins_dir" => "plugins", - "source" => "src", - "destination" => "output", - "collections_dir" => "", - "cache_dir" => ".bridgetown-cache", - "layouts_dir" => "_layouts", - "components_dir" => "_components", - "islands_dir" => "_islands", - "partials_dir" => "_partials", - "collections" => {}, - "taxonomies" => { + "root_dir" => Dir.pwd, + "plugins_dir" => "plugins", + "source" => "src", + "destination" => "output", + "collections_dir" => "", + "cache_dir" => ".bridgetown-cache", + "layouts_dir" => "_layouts", + "components_dir" => "_components", + "islands_dir" => "_islands", + "partials_dir" => "_partials", + "collections" => {}, + "taxonomies" => { category: { key: "categories", title: "Category" }, tag: { key: "tags", title: "Tag" }, }, - "autoload_paths" => [], - "inflector" => Bridgetown::Inflector.new, - "eager_load_paths" => [], - "autoloader_collapsed_paths" => [], - "additional_watch_paths" => [], + "autoload_paths" => [], + "inflector" => Bridgetown::Inflector.new, + "eager_load_paths" => [], + "autoloader_collapsed_paths" => [], + "additional_watch_paths" => [], # Handling Reading - "include" => [".htaccess", "_redirects", ".well-known"], - "exclude" => [], - "keep_files" => [".git", ".svn", "_bridgetown"], - "encoding" => "utf-8", - "markdown_ext" => "markdown,mkdown,mkdn,mkd,md", - "strict_front_matter" => false, - "slugify_mode" => "pretty", + "include" => [".htaccess", "_redirects", ".well-known"], + "exclude" => [], + "keep_files" => [".git", ".svn", "_bridgetown"], + "encoding" => "utf-8", + "markdown_ext" => "markdown,mkdown,mkdn,mkd,md", + "strict_front_matter" => false, + "slugify_mode" => "pretty", # Filtering Content - "future" => false, - "unpublished" => false, - "ruby_in_front_matter" => true, + "future" => false, + "unpublished" => false, + "ruby_in_front_matter" => true, # Conversion - "content_engine" => "resource", - "markdown" => "kramdown", - "highlighter" => "rouge", + "content_engine" => "resource", + "markdown" => "kramdown", + "highlighter" => "rouge", + "support_data_as_view_methods" => true, # Serving - "port" => "4000", - "host" => "127.0.0.1", - "base_path" => "/", - "show_dir_listing" => false, + "port" => "4000", + "host" => "127.0.0.1", + "base_path" => "/", + "show_dir_listing" => false, # Output Configuration - "available_locales" => [:en], - "default_locale" => :en, - "prefix_default_locale" => false, - "permalink" => nil, # default is set according to content engine - "timezone" => nil, # use the local timezone + "available_locales" => [:en], + "default_locale" => :en, + "prefix_default_locale" => false, + "permalink" => nil, # default is set according to content engine + "timezone" => nil, # use the local timezone - "quiet" => false, - "verbose" => false, - "defaults" => [], + "quiet" => false, + "verbose" => false, + "defaults" => [], - "liquid" => { + "liquid" => { "error_mode" => "warn", "strict_filters" => false, "strict_variables" => false, }, - "kramdown" => { + "kramdown" => { "auto_ids" => true, "toc_levels" => (1..6).to_a, "entity_output" => "as_char", @@ -104,7 +105,7 @@ def initialize(*) "mark_highlighting" => true, }, - "development" => { + "development" => { "fast_refresh" => true, }, }.each_with_object(Configuration.new) { |(k, v), hsh| hsh[k] = v.freeze }.freeze diff --git a/bridgetown-core/lib/bridgetown-core/drops/url_drop.rb b/bridgetown-core/lib/bridgetown-core/drops/url_drop.rb deleted file mode 100644 index 70640022d..000000000 --- a/bridgetown-core/lib/bridgetown-core/drops/url_drop.rb +++ /dev/null @@ -1,152 +0,0 @@ -# frozen_string_literal: true - -module Bridgetown - module Drops - class UrlDrop < Drop - extend Forwardable - - mutable false - - def_delegator :@obj, :cleaned_relative_path, :path - def_delegator :@obj, :output_ext, :output_ext - - def collection - @obj.collection.label - end - - def name - Utils.slugify(@obj.basename_without_ext) - end - - def title - Utils.slugify(qualified_slug_data, mode: "pretty", cased: true) - end - - def slug - Utils.slugify(qualified_slug_data) - end - - def locale - locale_data = @obj.data["locale"] - @obj.site.config["available_locales"].include?(locale_data) ? locale_data : nil - end - alias_method :lang, :locale - - def categories - category_set = Set.new - Array(@obj.data["categories"]).each do |category| - category_set << if @obj.site.config["slugify_categories"] - Utils.slugify(category.to_s) - else - category.to_s.downcase - end - end - category_set.to_a.join("/") - end - - # CCYY - def year - @obj.date.strftime("%Y") - end - - # MM: 01..12 - def month - @obj.date.strftime("%m") - end - - # DD: 01..31 - def day - @obj.date.strftime("%d") - end - - # hh: 00..23 - def hour - @obj.date.strftime("%H") - end - - # mm: 00..59 - def minute - @obj.date.strftime("%M") - end - - # ss: 00..59 - def second - @obj.date.strftime("%S") - end - - # D: 1..31 - def i_day - @obj.date.strftime("%-d") - end - - # M: 1..12 - def i_month - @obj.date.strftime("%-m") - end - - # MMM: Jan..Dec - def short_month - @obj.date.strftime("%b") - end - - # MMMM: January..December - def long_month - @obj.date.strftime("%B") - end - - # YY: 00..99 - def short_year - @obj.date.strftime("%y") - end - - # CCYYw, ISO week year - # may differ from CCYY for the first days of January and last days of December - def w_year - @obj.date.strftime("%G") - end - - # WW: 01..53 - # %W and %U do not comply with ISO 8601-1 - def week - @obj.date.strftime("%V") - end - - # d: 1..7 (Monday..Sunday) - def w_day - @obj.date.strftime("%u") - end - - # dd: Mon..Sun - def short_day - @obj.date.strftime("%a") - end - - # ddd: Monday..Sunday - def long_day - @obj.date.strftime("%A") - end - - # DDD: 001..366 - def y_day - @obj.date.strftime("%j") - end - - private - - def qualified_slug_data - slug_data = @obj.data["slug"] || @obj.basename_without_ext - if @obj.data["locale"] - slug_data.split(".").tap do |segments| - segments.pop if segments.length > 1 && segments.last == @obj.data["locale"] - end.join(".") - else - slug_data - end - end - - def fallback_data - @fallback_data ||= {} - end - end - end -end diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb index ea07efbc6..7b1da87c5 100644 --- a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb +++ b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/ruby.rb @@ -62,8 +62,8 @@ module Loaders # %}~~~ # ~~~~ class Ruby < Base - HEADER = %r!\A[~`#-]{3,}(?:ruby|<%|{%)\s*\n! - BLOCK = %r!#{HEADER.source}(.*?\n?)^((?:%>|%})?[~`#-]{3,}\s*$\n?)!m + HEADER = %r!\A[~`#-]{3,}(?:ruby|<%|{%)[ \t]*\n! + BLOCK = %r!#{HEADER.source}(.*?\n?)^((?:%>|%})?[~`#-]{3,}[ \t]*$\n?)!m # Determines whether a given file has Ruby front matter # @@ -77,9 +77,9 @@ def self.header?(file) def read(file_contents, file_path:) if (ruby_content = file_contents.match(BLOCK)) && should_execute_inline_ruby? Result.new( - content: ruby_content.post_match, + content: ruby_content.post_match.lstrip, front_matter: process_ruby_data(ruby_content[1], file_path, 2), - line_count: ruby_content[1].lines.size + line_count: ruby_content[1].lines.size - 1 ) elsif self.class.header?(file_path) Result.new( diff --git a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb index 11d639fe8..c915c0798 100644 --- a/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb +++ b/bridgetown-core/lib/bridgetown-core/front_matter/loaders/yaml.rb @@ -15,8 +15,8 @@ module Loaders # --- # ~~~ class YAML < Base - HEADER = %r!\A---\s*\n! - BLOCK = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m + HEADER = %r!\A---[ \t]*\n! + BLOCK = %r!#{HEADER.source}(.*?\n?)^((---|\.\.\.)[ \t]*$\n?)!m # Determines whether a given file has YAML front matter # @@ -31,7 +31,7 @@ def read(file_contents, **) yaml_content = file_contents.match(BLOCK) or return Result.new( - content: yaml_content.post_match, + content: yaml_content.post_match.lstrip, front_matter: YAMLParser.load(yaml_content[1]), line_count: yaml_content[1].lines.size - 1 ) diff --git a/bridgetown-core/lib/bridgetown-core/generated_page.rb b/bridgetown-core/lib/bridgetown-core/generated_page.rb index 18030543e..3ef910e9d 100644 --- a/bridgetown-core/lib/bridgetown-core/generated_page.rb +++ b/bridgetown-core/lib/bridgetown-core/generated_page.rb @@ -97,16 +97,30 @@ def permalink data&.permalink end + def add_permalink_suffix(template, permalink_style) + template = template.dup + + case permalink_style + when :pretty, :simple + template << "/" + else + template << "/" if permalink_style.to_s.end_with?("/") + template << ":output_ext" if permalink_style.to_s.end_with?(".*") + end + + template + end + # The template of the permalink. # # @return [String] def template if !html? - "/:path/:basename:output_ext" + "/:dir/:basename:output_ext" elsif index? - "/:path/" + "/:dir/" else - Utils.add_permalink_suffix("/:path/:basename", site.permalink_style) + add_permalink_suffix("/:dir/:basename", site.permalink_style) end end @@ -114,23 +128,25 @@ def template # # @return [String] def url - @url ||= URL.new( - template:, - placeholders: url_placeholders, - permalink: - ).to_s - end - alias_method :relative_url, :url + return @url if @url + + tmpl = permalink || template + placeholders = { dir: @dir, basename:, output_ext: } + + results = placeholders.inject(tmpl) do |result, token| + break result if result.index(":").nil? - # Returns a hash of URL placeholder names (as symbols) mapping to the - # desired placeholder replacements. For details see "url.rb" - def url_placeholders - { - path: @dir, - basename: @basename, - output_ext:, - } + if token.last.nil? + # Remove leading "/" to avoid generating urls with `//` + result.gsub("/:#{token.first}", "") + else + result.gsub(":#{token.first}", token.last) + end + end.then { Addressable::URI.normalize_component _1 } + + @url = "/#{results.sub("#", "%23")}".gsub("..", "/").gsub("./", "").squeeze("/") end + alias_method :relative_url, :url # Layout associated with this resource # This will output a warning if the layout can't be found. @@ -241,7 +257,7 @@ def place_into_layouts # # @return [String] def destination(dest) - path = site.in_dest_dir(dest, URL.unescape_path(url)) + path = site.in_dest_dir(dest, Utils.unencode_uri(url)) path = File.join(path, "index") if url.end_with?("/") path << output_ext unless path.end_with? output_ext path diff --git a/bridgetown-core/lib/bridgetown-core/model/base.rb b/bridgetown-core/lib/bridgetown-core/model/base.rb index ad7edd728..8b56555f8 100644 --- a/bridgetown-core/lib/bridgetown-core/model/base.rb +++ b/bridgetown-core/lib/bridgetown-core/model/base.rb @@ -3,6 +3,8 @@ module Bridgetown module Model class Base + include Bridgetown::RodaCallable + class << self def find(id, site: Bridgetown::Current.site) raise "A Bridgetown site must be initialized and added to Current" unless site @@ -92,6 +94,11 @@ def render_as_resource to_resource.read!.transform! end + # Converts this model to a resource and returns the full output + # + # @return [String] + def call(*) = render_as_resource.output + # override if need be # @return [Bridgetown::Site] def site diff --git a/bridgetown-core/lib/bridgetown-core/model/builder_origin.rb b/bridgetown-core/lib/bridgetown-core/model/builder_origin.rb index 5d0555654..acfe71bf7 100644 --- a/bridgetown-core/lib/bridgetown-core/model/builder_origin.rb +++ b/bridgetown-core/lib/bridgetown-core/model/builder_origin.rb @@ -34,9 +34,11 @@ def read @data end - def exists? - false - end + def front_matter_line_count = @data[:_front_matter_line_count_] + + def original_path = @data[:_original_path_] || relative_path + + def exists? = false def read_data_from_builder builder = Kernel.const_get(url.host.gsub(".", "::")) diff --git a/bridgetown-core/lib/bridgetown-core/rack/boot.rb b/bridgetown-core/lib/bridgetown-core/rack/boot.rb index 126acd124..6c9e7b1a3 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/boot.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/boot.rb @@ -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 diff --git a/bridgetown-core/lib/bridgetown-core/rack/routes.rb b/bridgetown-core/lib/bridgetown-core/rack/routes.rb index d656d4ea0..dcc5448fc 100644 --- a/bridgetown-core/lib/bridgetown-core/rack/routes.rb +++ b/bridgetown-core/lib/bridgetown-core/rack/routes.rb @@ -28,7 +28,7 @@ def print_routes end puts puts "Routes:" - puts "=======" + puts "=======\n" if routes.blank? puts "No routes found. Have you commented all of your routes?" puts "Documentation: https://github.com/jeremyevans/roda-route_list#basic-usage-" @@ -88,36 +88,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 diff --git a/bridgetown-core/lib/bridgetown-core/rack/static_indexes.rb b/bridgetown-core/lib/bridgetown-core/rack/static_indexes.rb deleted file mode 100644 index 276e134f1..000000000 --- a/bridgetown-core/lib/bridgetown-core/rack/static_indexes.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require "roda/plugins/public" - -# TODO: extract out to a standalone Roda plugin -Roda::RodaPlugins::Public::RequestMethods.module_eval do - SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact) # rubocop:disable Lint/ConstantDefinitionInBlock - def public_path_segments(path) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity - segments = [] - - path.split(SPLIT).each do |seg| - next if seg.empty? || seg == "." - - seg == ".." ? segments.pop : segments << seg - end - - path = File.join(roda_class.opts[:public_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 diff --git a/bridgetown-core/lib/bridgetown-core/resource/base.rb b/bridgetown-core/lib/bridgetown-core/resource/base.rb index 986d9eec0..4a9bb1e41 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/base.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/base.rb @@ -5,6 +5,7 @@ module Resource class Base # rubocop:todo Metrics/ClassLength using Bridgetown::Refinements include Comparable + include Bridgetown::RodaCallable include Bridgetown::Publishable include Bridgetown::LayoutPlaceable include Bridgetown::LiquidRenderable @@ -169,6 +170,11 @@ def transform! # rubocop:todo Metrics/CyclomaticComplexity self end + # Transforms the resource and returns the full output + # + # @return [String] + def call(*) = transform!.output + def trigger_hooks(hook_name, *args) Bridgetown::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection Bridgetown::Hooks.trigger :resources, hook_name, self, *args diff --git a/bridgetown-core/lib/bridgetown-core/resource/destination.rb b/bridgetown-core/lib/bridgetown-core/resource/destination.rb index 4f7f273da..d89f15d01 100644 --- a/bridgetown-core/lib/bridgetown-core/resource/destination.rb +++ b/bridgetown-core/lib/bridgetown-core/resource/destination.rb @@ -32,7 +32,7 @@ def final_ext end def output_path - path = URL.unescape_path(relative_url) + path = Utils.unencode_uri(relative_url) if resource.site.base_path.present? path = path.delete_prefix resource.site.base_path(strip_slash_only: true) end diff --git a/bridgetown-core/lib/bridgetown-core/ruby_template_view.rb b/bridgetown-core/lib/bridgetown-core/ruby_template_view.rb index ab4b50a63..36d453914 100644 --- a/bridgetown-core/lib/bridgetown-core/ruby_template_view.rb +++ b/bridgetown-core/lib/bridgetown-core/ruby_template_view.rb @@ -39,15 +39,16 @@ def initialize(convertible) end @paginator = resource.paginator if resource.respond_to?(:paginator) @site = resource.site + @support_data_as_view_methods = @site.config.support_data_as_view_methods end - def data - resource.data - end + def data = resource.data - def partial(_partial_name = nil, **_options) - raise "Must be implemented in a subclass" - end + def collections = site.collections + + def site_drop = site.site_payload.site + + def partial(_partial_name = nil, **_options) = raise("Must be implemented in a subclass") def render(item, **options, &) if item.respond_to?(:render_in) @@ -58,14 +59,6 @@ def render(item, **options, &) end end - def collections - site.collections - end - - def site_drop - site.site_payload.site - end - def liquid_render(component, options = {}, &block) options[:_block_content] = capture(&block) if block && respond_to?(:capture) render_statement = _render_statement(component, options) @@ -84,9 +77,17 @@ def helpers @helpers ||= Helpers.new(self, site) end + def data_key?(key, *args, **kwargs) + return false unless @support_data_as_view_methods + + args.empty? && kwargs.empty? && !block_given? && data.key?(key) + end + def method_missing(method_name, ...) if helpers.respond_to?(method_name.to_sym) helpers.send(method_name.to_sym, ...) + elsif data_key?(method_name, ...) + data[method_name] else super end @@ -96,9 +97,7 @@ def respond_to_missing?(method_name, include_private = false) helpers.respond_to?(method_name.to_sym, include_private) || super end - def inspect - "#<#{self.class} layout=#{layout&.label} resource=#{resource.relative_path}>" - end + def inspect = "#<#{self.class} layout=#{layout&.label} resource=#{resource.relative_path}>" private diff --git a/bridgetown-core/lib/bridgetown-core/static_file.rb b/bridgetown-core/lib/bridgetown-core/static_file.rb index a425c89f5..e3b0d4aa9 100644 --- a/bridgetown-core/lib/bridgetown-core/static_file.rb +++ b/bridgetown-core/lib/bridgetown-core/static_file.rb @@ -59,7 +59,7 @@ def destination(dest) if site.base_path.present? && collection dest_url = dest_url.delete_prefix site.base_path(strip_slash_only: true) end - site.in_dest_dir(dest, Bridgetown::URL.unescape_path(dest_url)) + site.in_dest_dir(dest, Bridgetown::Utils.unencode_uri(dest_url)) end def destination_rel_dir diff --git a/bridgetown-core/lib/bridgetown-core/url.rb b/bridgetown-core/lib/bridgetown-core/url.rb deleted file mode 100644 index 25cc10b05..000000000 --- a/bridgetown-core/lib/bridgetown-core/url.rb +++ /dev/null @@ -1,166 +0,0 @@ -# frozen_string_literal: true - -# Public: Methods that generate a URL for GeneratedPage. -# -# Examples -# -# URL.new({ -# :template => /:categories/:title.html", -# :placeholders => {:categories => "ruby", :title => "something"} -# }).to_s -# -module Bridgetown - # TODO: remove this class in favor of the new Resource permalink processor - class URL - # options - One of :permalink or :template must be supplied. - # :template - The String used as template for URL generation, - # for example "/:path/:basename:output_ext", where - # a placeholder is prefixed with a colon. - # :placeholders - A hash containing the placeholders which will be - # replaced when used inside the template. E.g. - # { "year" => Time.now.strftime("%Y") } would replace - # the placeholder ":year" with the current year. - # :permalink - If supplied, no URL will be generated from the - # template. Instead, the given permalink will be - # used as URL. - def initialize(options) - @template = options[:template] - @placeholders = options[:placeholders] || {} - @permalink = options[:permalink] - - return unless (@template || @permalink).nil? - - raise ArgumentError, "One of :template or :permalink must be supplied." - end - - # The generated relative URL of the resource - # - # Returns the String URL - def to_s - sanitize_url(generated_permalink || generated_url) - end - - # Generates a URL from the permalink - # - # Returns the _unsanitized String URL - def generated_permalink - (@generated_permalink ||= generate_url(@permalink)) if @permalink - end - - # Generates a URL from the template - # - # Returns the unsanitized String URL - def generated_url - @generated_url ||= generate_url(@template) - end - - # Internal: Generate the URL by replacing all placeholders with their - # respective values in the given template - # - # Returns the unsanitized String URL - def generate_url(template) - if @placeholders.is_a? Drops::UrlDrop - generate_url_from_drop(template) - else - generate_url_from_hash(template) - end - end - - def generate_url_from_hash(template) - @placeholders.inject(template) do |result, token| - break result if result.index(":").nil? - - if token.last.nil? - # Remove leading "/" to avoid generating urls with `//` - result.gsub("/:#{token.first}", "") - else - result.gsub(":#{token.first}", self.class.escape_path(token.last)) - end - end - end - - # We include underscores in keys to allow for 'i_month' and so forth. - # This poses a problem for keys which are followed by an underscore - # but the underscore is not part of the key, e.g. '/:month_:day'. - # That should be :month and :day, but our key extraction regexp isn't - # smart enough to know that so we have to make it an explicit - # possibility. - def possible_keys(key) - if key.end_with?("_") - [key, key.chomp("_")] - else - [key] - end - end - - def generate_url_from_drop(template) - template.gsub(%r!:([a-z_]+)!) do |match| - pool = possible_keys(match.sub(":", "")) - - winner = pool.find { |key| @placeholders.key?(key) } - if winner.nil? - raise NoMethodError, - "The URL template doesn't have #{pool.join(" or ")} keys. " \ - "Check your permalink template!" - end - - value = @placeholders[winner] - value = "" if value.nil? - replacement = self.class.escape_path(value) - - match.sub(":#{winner}", replacement) - end.squeeze("/") - end - - # Returns a sanitized String URL, stripping "../../" and multiples of "/", - # as well as the beginning "/" so we can enforce and ensure it. - - def sanitize_url(str) - "/#{str}".gsub("..", "/").gsub("./", "").squeeze("/") - end - - # Escapes a path to be a valid URL path segment - # - # path - The path to be escaped. - # - # Examples: - # - # URL.escape_path("/a b") - # # => "/a%20b" - # - # Returns the escaped path. - def self.escape_path(path) - path = path.to_s - return path if path.empty? || %r!^[a-zA-Z0-9./-]+$!.match?(path) - - # Because URI.escape doesn't escape "?", "[" and "]" by default, - # specify unsafe string (except unreserved, sub-delims, ":", "@" and "/"). - # - # URI path segment is defined in RFC 3986 as follows: - # segment = *pchar - # pchar = unreserved / pct-encoded / sub-delims / ":" / "@" - # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" - # pct-encoded = "%" HEXDIG HEXDIG - # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" - # / "*" / "+" / "," / ";" / "=" - Addressable::URI.encode(path).encode("utf-8").sub("#", "%23") - end - - # Unescapes a URL path segment - # - # path - The path to be unescaped. - # - # Examples: - # - # URL.unescape_path("/a%20b") - # # => "/a b" - # - # Returns the unescaped path. - def self.unescape_path(path) - path = path.encode("utf-8") - return path unless path.include?("%") - - Addressable::URI.unencode(path) - end - end -end diff --git a/bridgetown-core/lib/bridgetown-core/utils.rb b/bridgetown-core/lib/bridgetown-core/utils.rb index 849d5fe8e..74452c352 100644 --- a/bridgetown-core/lib/bridgetown-core/utils.rb +++ b/bridgetown-core/lib/bridgetown-core/utils.rb @@ -8,6 +8,7 @@ module Utils # rubocop:todo Metrics/ModuleLength autoload :RequireGems, "bridgetown-core/utils/require_gems" autoload :RubyExec, "bridgetown-core/utils/ruby_exec" autoload :SmartyPantsConverter, "bridgetown-core/utils/smarty_pants_converter" + autoload :PidTracker, "bridgetown-core/utils/pid_tracker" # Constants for use in #slugify SLUGIFY_MODES = %w(raw default pretty simple ascii latin).freeze @@ -35,6 +36,13 @@ def xml_escape(input) input.to_s.encode(xml: :attr).gsub(%r!\A"|"\Z!, "") end + def unencode_uri(path) + path = path.encode("utf-8") + return path unless path.include?("%") + + Addressable::URI.unencode(path) + end + # Non-destructive version of deep_merge_hashes! See that method. # # Returns the merged hashes. @@ -213,49 +221,6 @@ def slugify(string, mode: nil, cased: false) slug end - # Add an appropriate suffix to template so that it matches the specified - # permalink style. - # - # template - permalink template without trailing slash or file extension - # permalink_style - permalink style, either built-in or custom - # - # The returned permalink template will use the same ending style as - # specified in permalink_style. For example, if permalink_style contains a - # trailing slash (or is :pretty, which indirectly has a trailing slash), - # then so will the returned template. If permalink_style has a trailing - # ":output_ext" (or is :none, :date, or :ordinal) then so will the returned - # template. Otherwise, template will be returned without modification. - # - # Examples: - # add_permalink_suffix("/:basename", :pretty) - # # => "/:basename/" - # - # add_permalink_suffix("/:basename", :date) - # # => "/:basename:output_ext" - # - # add_permalink_suffix("/:basename", "/:year/:month/:title/") - # # => "/:basename/" - # - # add_permalink_suffix("/:basename", "/:year/:month/:title") - # # => "/:basename" - # - # Returns the updated permalink template - def add_permalink_suffix(template, permalink_style) - template = template.dup - - case permalink_style - when :pretty, :simple - template << "/" - when :date, :ordinal, :none - template << ":output_ext" - else - template << "/" if permalink_style.to_s.end_with?("/") - template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext") - end - - template - end - # Work the same way as Dir.glob but seperating the input into two parts # ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act # as a pattern. diff --git a/bridgetown-core/lib/bridgetown-core/utils/aux.rb b/bridgetown-core/lib/bridgetown-core/utils/aux.rb index 02a795c9b..864183f42 100644 --- a/bridgetown-core/lib/bridgetown-core/utils/aux.rb +++ b/bridgetown-core/lib/bridgetown-core/utils/aux.rb @@ -3,20 +3,14 @@ module Bridgetown module Utils module Aux + extend Bridgetown::Utils::PidTracker + def self.with_color(name, message) return message unless !name.nil? && Bridgetown::Foundation::Ansi::COLORS[name.to_sym] Bridgetown::Foundation::Ansi.send(name, message) end - def self.running_pids - @running_pids ||= [] - end - - def self.add_pid(pid) - running_pids << pid - end - def self.run_process(name, color, cmd, env: {}) @mutex ||= Thread::Mutex.new @@ -24,7 +18,7 @@ def self.run_process(name, color, cmd, env: {}) rd, wr = IO.pipe("BINARY") pid = Process.spawn({ "BRIDGETOWN_NO_BUNDLER_REQUIRE" => nil }.merge(env), cmd, out: wr, err: wr, pgroup: true) - @mutex.synchronize { add_pid(pid) } + @mutex.synchronize { add_pid(pid, file: :aux) } loop do line = rd.gets @@ -45,9 +39,12 @@ def self.run_process(name, color, cmd, env: {}) def self.kill_processes Bridgetown.logger.info "Stopping auxiliary processes..." - running_pids.each do |pid| - Process.kill("SIGTERM", -Process.getpgid(pid)) + + read_pidfile(:aux).each do |pid| + Process.kill("SIGTERM", -Process.getpgid(pid.to_i)) rescue Errno::ESRCH, Errno::EPERM, Errno::ECHILD # rubocop:disable Lint/SuppressedException + ensure + remove_pidfile :aux end end end diff --git a/bridgetown-core/lib/bridgetown-core/utils/pid_tracker.rb b/bridgetown-core/lib/bridgetown-core/utils/pid_tracker.rb new file mode 100644 index 000000000..067bab453 --- /dev/null +++ b/bridgetown-core/lib/bridgetown-core/utils/pid_tracker.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Bridgetown + module Utils + module PidTracker + def create_pid_dir + FileUtils.mkdir_p pids_dir + end + + def add_pid(pid, file:) + File.write pidfile_for(file), "#{pid}\n", mode: "a+" + end + + def read_pidfile(file) + File.readlines pidfile_for(file), chomp: true + end + + def remove_pidfile(file) + File.delete pidfile_for(file) + end + + private + + def root_dir + Dir.pwd + end + + def pids_dir + File.join(root_dir, "tmp", "pids") + end + + def pidfile_for(file) + File.join(pids_dir, "#{file}.pid") + end + end + end +end diff --git a/bridgetown-core/lib/bridgetown-core/watcher.rb b/bridgetown-core/lib/bridgetown-core/watcher.rb index cccc9d387..4f9438c1d 100644 --- a/bridgetown-core/lib/bridgetown-core/watcher.rb +++ b/bridgetown-core/lib/bridgetown-core/watcher.rb @@ -25,11 +25,11 @@ def watch(site, options, &block) Bridgetown::Hooks.trigger :site, :post_read, site block&.call(site) end - end - Bridgetown.logger.info "Watcher:", "enabled." unless options[:using_puma] + return + end - return if options[:serving] + Bridgetown.logger.info "Watcher:", "enabled." unless options[:start_command] trap("INT") do self.shutdown = true diff --git a/bridgetown-core/lib/roda/plugins/bridgetown_server.rb b/bridgetown-core/lib/roda/plugins/bridgetown_server.rb index 222c30898..15e08df4a 100644 --- a/bridgetown-core/lib/roda/plugins/bridgetown_server.rb +++ b/bridgetown-core/lib/roda/plugins/bridgetown_server.rb @@ -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 @@ -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")) @@ -40,17 +41,7 @@ def self.load_dependencies(app) # rubocop:disable Metrics "500 Internal Server Error" end - # This lets us return models or resources directly in Roda response blocks - app.plugin :custom_block_results - - app.handle_block_result Bridgetown::Model::Base do |result| - result.render_as_resource.output - end - - app.handle_block_result Bridgetown::Resource::Base do |result| - 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 @@ -107,8 +98,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] = @@ -117,11 +116,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 "
ERROR: cannot find index.html
in the output folder.
Locale: en
" end should "have the correct permalink and locale in French" do assert_equal "/fr/second-level-page/", @french_resource.relative_url + assert_includes @french_resource.output, "Câest bien.
\n\nLocale: fr
" assert_includes @french_resource.output, <<-HTML diff --git a/bridgetown-core/test/test_ssr.rb b/bridgetown-core/test/test_ssr.rb index 36fd247e3..69fe97447 100644 --- a/bridgetown-core/test/test_ssr.rb +++ b/bridgetown-core/test/test_ssr.rb @@ -85,5 +85,13 @@ def site assert last_response.ok? assert_includes last_response.body, "THIS IS A TEST.
" end + + should "return rendered component" do + get "/render_component/wow" + + assert last_response.ok? + assert_equal "application/rss+xml", last_response["Content-Type"] + assert_equal "