Skip to content

Commit

Permalink
Experimental: Fast refresh feature in development (#872)
Browse files Browse the repository at this point in the history
* Add basic mechanism for fast refresh of changed content

* Fast refresh support for resources && paginated pages

* Resolve test suite issues by adding conditional logic to `members`

Also add Enumerable support

* update website lock

* Fix double-transform bug for stability

* clear error file at start of refresh

* Fast refresh of layouts now supported

* Add support for Fast Refresh of partials

* Support components for Fash Refresh

* Fix long-standing generators test instability

* Add feature tests for Fast Refresh

* Fix fast refresh taxonomy issue

* Abort fast refresh if the resource's taxonomy changed

* Add helpful error messaging around removed `webpack_path`

* Add new Enumerable APIs to Collection

* fix typo

* add `find`

* fix logic bug with nested components

* add `bypass_tracking` helper, improve error logging

* Revert "Add new Enumerable APIs to Collection"

This reverts commit f8519ca.

* Simplify the logic to get at a generated page's original resource

* Make Collections "enumerable"

* Fix test

* Update initializers template, get tests passing
  • Loading branch information
jaredcwhite authored Apr 12, 2024
1 parent 5aed936 commit 6e0751a
Show file tree
Hide file tree
Showing 33 changed files with 879 additions and 92 deletions.
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ PATH
roda (~> 3.46)
rouge (>= 3.0, < 5.0)
serbea (~> 1.0)
signalize (~> 1.3)
thor (~> 1.1)
tilt (~> 2.0)
zeitwerk (~> 2.5)
Expand Down Expand Up @@ -184,6 +185,8 @@ GEM
shoulda-context (2.0.0)
shoulda-matchers (4.5.1)
activesupport (>= 4.2.0)
signalize (1.3.0)
concurrent-ruby (~> 1.2)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
Expand Down
22 changes: 11 additions & 11 deletions bridgetown-builder/test/test_generators.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class GeneratorBuilder < Builder

def build
generator do
site.data[:site_metadata][:title] = "Test Title"
site.signals[:site_metadata][:title] = "Test Title"
end
end
end
Expand All @@ -17,26 +17,26 @@ class GeneratorBuilder2 < Builder

def build
generator do
site.data[:site_metadata][:title] = "Test Title 2"
site.signals[:site_metadata][:title] = "Test Title 2"
end
end
end

class TestGenerators < BridgetownUnitTest
context "creating a generator" do
setup do
@site = Site.new(site_configuration)
should "be loaded on site setup" do
@builders = [GeneratorBuilder, GeneratorBuilder2].sort
@site = Site.new(site_configuration)
@site.signals[:site_metadata] = { title: "Initial Value" }

funcs = []
@builders.each_with_index do |builder, index|
builder.new("Generator Test #{index}", @site).build_with_callbacks
builder.new("Generator Test #{index}", @site).build_with_callbacks.tap do |b|
funcs += b.functions.to_a
end
end
end

should "be loaded on site setup" do
@site.reset
@site.data[:site_metadata] = { title: "Initial Value" }
@site.loaders_manager.unload_loaders
@site.setup
assert_equal 2, (@site.generators.map(&:class) & funcs.map { _1[:generator] }).length
@site.generate

assert_equal "Test Title", @site.metadata[:title]
Expand Down
9 changes: 2 additions & 7 deletions bridgetown-builder/test/test_method_symbols.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ def build
end

def set_title
# TODO: had a weird testing issue where the data was missing :site_metadata
# even after setting it the test before generation. Spent a long time trying
# to track this down…seemed to be a non-determinisitic artifact of the testing
# process itself. Finally gave up on it. Hoping something odd like that
# doesn't come up in actual CLI usage! -JW
site.data[:site_metadata] = { title: "Test Title in Method Symbols" }
site.signals[:site_metadata] = { title: "Test Title in Method Symbols" }
end

def upcase_tag(attributes, tag)
Expand All @@ -41,7 +36,7 @@ class TestMethodSymbols < BridgetownUnitTest

should "load generator on site generate" do
@site.reset
@site.data[:site_metadata] = { title: "Initial Value in Method Symbols" }
@site.signals[:site_metadata] = { title: "Initial Value in Method Symbols" }
@site.loaders_manager.unload_loaders
@site.setup

Expand Down
1 change: 1 addition & 0 deletions bridgetown-core/bridgetown-core.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Gem::Specification.new do |s|
s.add_runtime_dependency("roda", "~> 3.46")
s.add_runtime_dependency("rouge", [">= 3.0", "< 5.0"])
s.add_runtime_dependency("serbea", "~> 1.0")
s.add_runtime_dependency("signalize", "~> 1.3")
s.add_runtime_dependency("thor", "~> 1.1")
s.add_runtime_dependency("tilt", "~> 2.0")
s.add_runtime_dependency("zeitwerk", "~> 2.5")
Expand Down
2 changes: 2 additions & 0 deletions bridgetown-core/lib/bridgetown-core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def require_all(path)
require "i18n"
require "i18n/backend/fallbacks"
require "faraday"
require "signalize"
require "thor"
require "zeitwerk"

Expand Down Expand Up @@ -99,6 +100,7 @@ module Bridgetown
autoload :Reader, "bridgetown-core/reader"
autoload :RubyTemplateView, "bridgetown-core/ruby_template_view"
autoload :LogWriter, "bridgetown-core/log_writer"
autoload :Signals, "bridgetown-core/signals"
autoload :Site, "bridgetown-core/site"
autoload :Slot, "bridgetown-core/slot"
autoload :StaticFile, "bridgetown-core/static_file"
Expand Down
8 changes: 4 additions & 4 deletions bridgetown-core/lib/bridgetown-core/collection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

module Bridgetown
class Collection
include Enumerable

# @return [Bridgetown::Site]
attr_reader :site

Expand Down Expand Up @@ -49,10 +51,8 @@ def resources_by_relative_url
resources.group_by(&:relative_url).transform_values(&:first)
end

# Iterate over Resources
def each(&)
resources.each(&)
end
# Iterate over Resources, support Enumerable
def each(...) = resources.each(...)

# Fetch the static files in this collection.
# Defaults to an empty array if no static files have been read in.
Expand Down
1 change: 1 addition & 0 deletions bridgetown-core/lib/bridgetown-core/commands/build.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def build
build_site(config_options)
end

# TODO: remove this logic…I can't find "detach" anywhere
if config_options.fetch("detach", false)
Bridgetown.logger.info "Auto-regeneration:",
"disabled when running server detached."
Expand Down
8 changes: 7 additions & 1 deletion bridgetown-core/lib/bridgetown-core/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def supported_template_extensions
end

def path_for_errors
component_template_path
File.basename(component_template_path)
rescue RuntimeError
source_location
end
Expand Down Expand Up @@ -181,6 +181,12 @@ def render_in(view_context, &block)
@_content_block = block

if render?
if helpers.site.config.fast_refresh
signal = helpers.site.tmp_cache["comp-signal:#{self.class.source_location}"] ||=
Signalize.signal(1)
# subscribe so resources are attached to this component within effect
signal.value
end
before_render
template
else
Expand Down
7 changes: 3 additions & 4 deletions bridgetown-core/lib/bridgetown-core/concerns/site/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ def categories
taxonomies.category
end

# Returns the value of `data["site_metadata"]` or creates a new instance of
# `HashWithDotAccess::Hash`
# @return [Hash] Returns a hash of site metadata
# Returns the contents of the site metadata file or a blank hash
# @return [HashWithDotAccess::Hash] Returns a hash of site metadata
def metadata
data["site_metadata"] ||= HashWithDotAccess::Hash.new
signals["site_metadata"] ||= HashWithDotAccess::Hash.new
end

# The Hash payload containing site-wide data.
Expand Down
158 changes: 158 additions & 0 deletions bridgetown-core/lib/bridgetown-core/concerns/site/fast_refreshable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true

class Bridgetown::Site
module FastRefreshable
def fast_refresh(paths = [], reload_if_needed: false) # rubocop:todo Metrics
FileUtils.rm_f(Bridgetown.build_errors_path)

@fast_refresh_ordering = 0
full_abort = false
found_gen_pages = false
paths.each do |path|
res = resources.find do |resource|
resource.id.start_with?("repo://") && in_source_dir(resource.relative_path) == path
end

layouts_to_reload = Set.new
locate_resource_layouts_and_partials_for_fash_refresh(path, layouts_to_reload) unless res

locate_components_for_fast_refresh(path) unless res

pages = locate_layouts_and_pages_for_fast_refresh(path, layouts_to_reload)

layouts_to_reload.each do |layout|
layouts[layout.label] = Bridgetown::Layout.new(
self, layout.instance_variable_get(:@base), layout.name
)
end
liquid_renderer.reset unless layouts_to_reload.empty?
next unless res || !pages.empty?

unless pages.empty?
found_gen_pages = true
mark_original_page_resources_for_fast_refresh(pages)
next
end

res.prepare_for_fast_refresh!.tap { full_abort = true unless _1 }
next unless res.collection.data?

res.collection.merge_data_resources.each do |k, v|
data[k] = v
signals[k] = v
end
end

marked_resources = resources.select(&:fast_refresh_order).sort_by(&:fast_refresh_order)
if full_abort || (marked_resources.empty? && !found_gen_pages)
# Darn, a full reload is needed (unless we're on a super-fast track)
if reload_if_needed
Bridgetown::Hooks.trigger :site, :pre_reload, self, paths
Bridgetown::Hooks.clear_reloadable_hooks
loaders_manager.reload_loaders
Bridgetown::Hooks.trigger :site, :post_reload, self, paths
process # bring out the big guns
end
return
end

Bridgetown::Hooks.trigger :site, :fast_refresh, self

transform_resources_for_fast_refresh(marked_resources, found_gen_pages)
transform_generated_pages_for_fast_refresh

FileUtils.touch(in_destination_dir("index.html"))

Bridgetown::Hooks.trigger :site, :post_write, self
end

private

def locate_resource_layouts_and_partials_for_fash_refresh(path, layouts_to_reload) # rubocop:todo Metrics/AbcSize
resources.each do |resource|
next unless resource.data.layout

res_layouts = validated_layouts_for(resource, resource.data.layout)
.select { _1.path == path }
next unless res_layouts.length.positive?

res_layouts.each { layouts_to_reload << _1 }
resource.mark_for_fast_refresh!
end

tmp_cache.each_key do |key|
next unless key.delete_prefix("partial-tmpl:") == path

tmp_cache[key].template = nil
tmp_cache[key].signal.value += 1
end
end

def locate_components_for_fast_refresh(path)
comp = Bridgetown::Component.subclasses.find do |item|
item.component_template_path == path || item.source_location == path
end
return unless comp

tmp_cache["comp-signal:#{comp.source_location}"]&.value += 1

# brute force reload all components for now
load_path = config.components_load_paths.last
loader = loaders_manager.loaders[load_path]
Bridgetown::Hooks.trigger :loader, :pre_reload, loader, load_path
loader.reload
loader.eager_load if config.eager_load_paths.include?(load_path)
Bridgetown::Hooks.trigger :loader, :post_reload, loader, load_path
end

def locate_layouts_and_pages_for_fast_refresh(path, layouts_to_reload)
generated_pages.select do |pg|
next unless pg.respond_to?(:page_to_copy)

found = in_source_dir(pg.original_resource.relative_path) == path
next true if found
next false unless pg.data.layout

pg_layouts = validated_layouts_for(pg, pg.data.layout)
.select { _1.path == path }
next false unless pg_layouts.length.positive?

pg_layouts.each { layouts_to_reload << _1 }
true
end
end

def mark_original_page_resources_for_fast_refresh(pages)
pages.each do |page|
res = page.original_resource
res.prepare_for_fast_refresh! unless res.fast_refresh_order
page.mark_for_fast_refresh!
end
end

def transform_resources_for_fast_refresh(marked_resources, found_gen_pages)
marked_resources.each { _1.transform!.write }
number_of_resources = marked_resources.length
number_of_resources += 1 if found_gen_pages
Bridgetown.logger.info(
"⚡️",
"#{number_of_resources} resource#{"s" if number_of_resources > 1} fast refreshed"
)
end

def transform_generated_pages_for_fast_refresh
marked_generated = generated_pages.select(&:fast_refresh_order).sort_by(&:fast_refresh_order)
return if marked_generated.empty?

marked_generated.each do |page|
page.fast_refresh! if page.respond_to?(:fast_refresh!)
page.transform!.write(dest)
end
number_of_pages = marked_generated.length
Bridgetown.logger.info(
"⚡️",
"#{number_of_pages} generated page#{"s" if number_of_pages > 1} fast refreshed"
)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ def reset(soft: false) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
self.generated_pages = []
self.static_files = []
self.data = HashWithDotAccess::Hash.new unless soft
@fast_refresh_ordering = 0 if config.fast_refresh
@frontend_manifest = nil
@collections = nil
@documents = nil
Expand Down
4 changes: 4 additions & 0 deletions bridgetown-core/lib/bridgetown-core/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ def initialize(*)
"include_extraction_tags" => false,
"mark_highlighting" => true,
},

"development" => {
"fast_refresh" => true,
},
}.each_with_object(Configuration.new) { |(k, v), hsh| hsh[k] = v.freeze }.freeze

# TODO: Deprecated. Remove support for _config as well as toml in the next release.
Expand Down
7 changes: 7 additions & 0 deletions bridgetown-core/lib/bridgetown-core/converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ def line_start(convertible)
if convertible.is_a?(Bridgetown::Resource::Base) &&
convertible.model.origin.respond_to?(:front_matter_line_count)
convertible.model.origin.front_matter_line_count + 4
elsif convertible.is_a?(Bridgetown::GeneratedPage) && convertible.original_resource
res = convertible.original_resource
if res.model.origin.respond_to?(:front_matter_line_count)
res.model.origin.front_matter_line_count + 4
else
1
end
else
1
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,18 @@ def partial(partial_name = nil, **options, &block)

def _render_partial(partial_name, options)
partial_path = _partial_path(partial_name, "erb")
tmpl = site.tmp_cache["partial-tmpl:#{partial_path}"] ||= Tilt::ErubiTemplate.new(
site.tmp_cache["partial-tmpl:#{partial_path}"] ||= {
signal: site.config.fast_refresh ? Signalize.signal(1) : nil,
}
tmpl = site.tmp_cache["partial-tmpl:#{partial_path}"]
tmpl.template ||= Tilt::ErubiTemplate.new(
partial_path,
outvar: "@_erbout",
bufval: "Bridgetown::OutputBuffer.new",
engine_class: ERBEngine
)
tmpl.render(self, options)
tmpl.signal&.value # subscribe so resources are attached to this partial within effect
tmpl.template.render(self, options)
end
end

Expand Down
Loading

0 comments on commit 6e0751a

Please sign in to comment.