pvincent
2 months ago
20 changed files with 199 additions and 326 deletions
-
2app/views/layouts/application.html.erb
-
3app/views/scores/index.html.erb
-
20config/application.rb
-
17config/environments/development.rb
-
9config/initializers/hot_changes.rb
-
14config/initializers/monkey_patcher.rb
-
12config/initializers/monkey_patches.rb
-
13config/initializers/rails_live_reload.rb
-
21config/initializers/semantic_logger.rb
-
104lib/hot/constants.rb
-
8lib/hot/live.rb
-
13lib/live/constants.rb
-
118lib/live/definable.rb
-
17lib/monkey_patches/monkey_patcher.rb
-
29lib/monkey_patches/rails_live_reload/watcher.rb
-
4lib/semantic/basic_formatter.rb
-
76lib/semantic/dev_loader.rb
-
2lib/semantic/fancy_dimensions.rb
-
4lib/semantic/fancy_formatter.rb
-
39lib/semantic/notification_util.rb
@ -1,9 +0,0 @@ |
|||||
return unless Rails.application.server? |
|
||||
|
|
||||
Rails.application.config.after_initialize do |
|
||||
Hot::Constants.on_change(:log_active_record) { |bool| ActiveRecord::Base.logger.level = bool ? :debug : :fatal } |
|
||||
Hot::Constants.on_change(:log_action_view) do |bool| |
|
||||
ActionView::Base.logger.level = bool ? :debug : :fatal |
|
||||
Rails.application.config.dev_loader.launch |
|
||||
end |
|
||||
end |
|
@ -1,14 +0,0 @@ |
|||||
return unless Rails.application.server? |
|
||||
|
|
||||
# puts 'MonkeyPatcher runs:' |
|
||||
patches = Dir.glob(Rails.root.join('lib', 'monkey_patches', '**', '*.rb')) |
|
||||
patches.each do |file| |
|
||||
# puts "🐵 patching... #{Pathname.new(file).relative_path_from Rails.root}" |
|
||||
require file |
|
||||
end |
|
||||
|
|
||||
# puts case patches.count |
|
||||
# when 0 then 'No patch found' |
|
||||
# when 1 then '1 successful patch applied' |
|
||||
# else "#{patches.count} successful patches applied" |
|
||||
# end |
|
@ -0,0 +1,12 @@ |
|||||
|
return unless Rails.application.server? || Rails.application.console? |
||||
|
|
||||
|
require_relative '../../lib/monkey_patches/monkey_patcher' |
||||
|
|
||||
|
MonkeyPatcher.run do |patch| |
||||
|
case patch |
||||
|
when 'action_dispatch/middleware/debug_exceptions.rb' then Rails.application.server? |
||||
|
when 'rails_live_reload/watcher.rb' then Rails.application.server? && Rails.env.development? |
||||
|
when /^semantic/ then true |
||||
|
else false |
||||
|
end |
||||
|
end |
@ -1,16 +1,11 @@ |
|||||
return unless defined?(RailsLiveReload) && Rails.env.development? |
return unless defined?(RailsLiveReload) && Rails.env.development? |
||||
|
|
||||
|
# RailsLiveReload |
||||
RailsLiveReload.configure do |config| |
RailsLiveReload.configure do |config| |
||||
# HOT Constants |
|
||||
config.watch(%r{/\.env$}, reload: :always) |
|
||||
config.watch(%r{/\.env\.sample$}) |
|
||||
# config.watch(%r{/config/initializers/hot_changes.rb$}) |
|
||||
|
|
||||
# USEFUL for tailwind changes!!!! |
|
||||
config.watch %r{app/assets/builds/tailwind.css}, reload: :always |
|
||||
|
|
||||
# Rk: prevent any reload from files ending with '.tailwind.css' |
|
||||
|
# Tailwind CSS |
||||
|
# First rule prevents reloading from asset files ending with '.tailwind.css' |
||||
config.watch %r{(app|vendor)/(assets|javascript)/.+\.(css|js|html|png|jpg)(?<!tailwind\.css)$}, reload: :always |
config.watch %r{(app|vendor)/(assets|javascript)/.+\.(css|js|html|png|jpg)(?<!tailwind\.css)$}, reload: :always |
||||
|
config.watch %r{app/assets/builds/tailwind.css}, reload: :always |
||||
|
|
||||
# Default watched folders & files |
# Default watched folders & files |
||||
config.watch %r{app/views/.+\.(erb|haml|slim)$} |
config.watch %r{app/views/.+\.(erb|haml|slim)$} |
||||
|
@ -0,0 +1,21 @@ |
|||||
|
RailsSemanticLogger::Rack::Logger.logger.level = :info # useful for remaining log like "[Rack::Log] Started..." |
||||
|
SemanticLogger.clear_appenders! |
||||
|
|
||||
|
# Zeitwerk reload message |
||||
|
Rails.autoloaders.main.on_load('ApplicationController') { SemanticLogger[:Zeitwerk].debug('reload!') } |
||||
|
|
||||
|
all_notifications = { |
||||
|
action_controller: %i[start_processing process_action redirect_to], |
||||
|
action_view: %i[render_partial render_template render_collection render_layout], |
||||
|
active_record: %i[sql strict_loading instantiation start_transaction transaction] |
||||
|
} |
||||
|
|
||||
|
Rails.configuration.after_initialize do |
||||
|
all_notifications.each do |event_group, hooks| |
||||
|
hooks.each { |hook| ActiveSupport::Notifications.unsubscribe("#{hook}.#{event_group}") } |
||||
|
end |
||||
|
|
||||
|
ActiveSupport::Notifications.subscribe('rolling.live_constant') do |event| |
||||
|
SemanticLogger[:live_notifications].warn('new event', event.payload) # FIXME: to be continued... |
||||
|
end |
||||
|
end |
@ -1,104 +0,0 @@ |
|||||
require 'dotenv' |
|
||||
|
|
||||
module Hot |
|
||||
# Hot Live Constants |
|
||||
module Constants |
|
||||
include SemanticLogger::Loggable |
|
||||
HOTENV = {} # rubocop:disable Style/MutableConstant |
|
||||
LISTENERS = {} # rubocop:disable Style/MutableConstant |
|
||||
|
|
||||
class << self |
|
||||
def initialize |
|
||||
@old_definitions = {} |
|
||||
@hot_definitions = {} |
|
||||
|
|
||||
logger.level = :fatal |
|
||||
load_definitions |
|
||||
load_values |
|
||||
logger.level = :info |
|
||||
end |
|
||||
|
|
||||
def load_definitions |
|
||||
logger.debug('--load_definitions3') |
|
||||
|
|
||||
new_definitions = Dotenv.parse('.env.sample') |
|
||||
|
|
||||
definitions_to_actualize = {} |
|
||||
@old_definitions.merge(new_definitions) do |key, old_value, new_value| |
|
||||
definitions_to_actualize[key] = new_value if new_value != old_value |
|
||||
end |
|
||||
definitions_to_actualize.each { |key, value| define_method(:actualize, key, value) } |
|
||||
|
|
||||
@old_definitions = new_definitions |
|
||||
|
|
||||
definitions_to_remove = @hot_definitions.except(*new_definitions.keys) |
|
||||
definitions_to_remove.each_pair do |key, dkey| |
|
||||
logger.info("remove method <#{dkey}>") |
|
||||
singleton_class.undef_method(dkey) |
|
||||
@hot_definitions.delete(key) |
|
||||
end |
|
||||
|
|
||||
definitions_to_add = new_definitions.except(*@hot_definitions.keys) |
|
||||
definitions_to_add.each { |key, value| define_method(:add, key, value) } |
|
||||
end |
|
||||
|
|
||||
def load_values |
|
||||
logger.debug('--load_values') |
|
||||
|
|
||||
new_env = Dotenv.parse |
|
||||
constants_to_delete = HOTENV.except(*new_env.keys) |
|
||||
constants_to_delete.each do |name, _| |
|
||||
# FIXME: default should read default and type from @old_definitions |
|
||||
type, default = @old_definitions[name] |
|
||||
logger.info("constant <#{name}> reverts to default value <#{default}> of type <#{type}>") |
|
||||
end |
|
||||
|
|
||||
constants_to_add = new_env.except(*HOTENV.keys) |
|
||||
constants_to_add.each do |constant| |
|
||||
logger.info("constant to add <#{constant}>") |
|
||||
end |
|
||||
|
|
||||
HOTENV.replace new_env |
|
||||
LISTENERS.each_pair { |k, b| perform_change(k, b) } |
|
||||
end |
|
||||
|
|
||||
def on_change(key, &block) |
|
||||
LISTENERS[key.downcase.to_sym] = block |
|
||||
perform_change(key, block) |
|
||||
end |
|
||||
|
|
||||
private |
|
||||
|
|
||||
def define_method(mode, key, value) |
|
||||
dkey = key.downcase |
|
||||
method = infer_method_from_value(value) |
|
||||
|
|
||||
inferred_type = method.name.to_s.split('_')[1] |
|
||||
logger.info("#{mode} method <#{dkey}> of type <#{inferred_type}> with default value <#{value}> ") |
|
||||
|
|
||||
singleton_class.define_method(dkey) { method.call(key, value) } |
|
||||
@hot_definitions.store(key, dkey) |
|
||||
end |
|
||||
|
|
||||
def perform_change(key, block) |
|
||||
old_value = nil # TODO: remember last previous value |
|
||||
new_value = method(key).call |
|
||||
block.call(new_value, old_value) |
|
||||
end |
|
||||
|
|
||||
def load_boolean(key, default) = HOTENV.fetch(key, default).to_s.downcase == 'true' |
|
||||
def load_integer(key, default) = HOTENV.fetch(key, default).to_i |
|
||||
def load_string(key, default) = HOTENV.fetch(key, default) |
|
||||
|
|
||||
def infer_method_from_value(value) |
|
||||
case value.downcase |
|
||||
when /^(true|false)$/ then method(:load_boolean) |
|
||||
when /^\d+$/ then method(:load_integer) |
|
||||
else method(:load_string) |
|
||||
end |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
initialize # done once on first require, cause this is just a module (not a class!) |
|
||||
end |
|
||||
end |
|
@ -1,8 +0,0 @@ |
|||||
module Hot |
|
||||
# Hot Live constants |
|
||||
class Live |
|
||||
def initialize |
|
||||
puts 'Hot Live constants initialized' |
|
||||
end |
|
||||
end |
|
||||
end |
|
@ -0,0 +1,13 @@ |
|||||
|
module Live |
||||
|
# My live constants |
||||
|
module Constants |
||||
|
extend Definable |
||||
|
|
||||
|
STIMULUS_DEBUG = boolean false |
||||
|
ACTION_VIEW = boolean true |
||||
|
ACTION_CONTROLLER = boolean true |
||||
|
|
||||
|
MY_INTEGER = integer 8 |
||||
|
MY_STRING = string 'titi' |
||||
|
end |
||||
|
end |
@ -0,0 +1,118 @@ |
|||||
|
require 'dotenv' |
||||
|
|
||||
|
$definable_thread_group ||= ThreadGroup.new |
||||
|
|
||||
|
module Live |
||||
|
# offers typed constant defintions with default value, by using lots of introspecting... |
||||
|
module Definable |
||||
|
MAIN_CSS = 'app/assets/stylesheets/application.css'.freeze # useful to trigger :reload_all from RailsLiveReload |
||||
|
|
||||
|
def integer(default = 0) = define_type_from_callee(caller[0], :integer, default) |
||||
|
def boolean(default = true) = define_type_from_callee(caller[0], :boolean, default) # rubocop:disable Style/OptionalBooleanParameter |
||||
|
def string(default = '') = define_type_from_callee(caller[0], :string, default) |
||||
|
|
||||
|
def reload_from_env |
||||
|
logger.debug('reload from env') |
||||
|
|
||||
|
@env_values = Dotenv.parse(*Dotenv::Rails.files) |
||||
|
changes = 0 |
||||
|
prefix = 'Constant' |
||||
|
env_values.each_pair do |constant, raw| |
||||
|
next unless definitions.include?(constant) |
||||
|
|
||||
|
value = typed_value(definitions[constant][:type], raw, definitions[constant][:default]) |
||||
|
next unless value != definitions[constant][:value] |
||||
|
|
||||
|
define_value(constant, value) |
||||
|
logger.warn "#{prefix} overriden from environment:#{Semantic::AnsiColors::CLEAR} #{constant} = #{value.ai}" |
||||
|
changes += 1 |
||||
|
end |
||||
|
definitions.except(*env_values.keys).each_pair do |constant, options| |
||||
|
default = options[:default] |
||||
|
next unless options[:value] != default |
||||
|
|
||||
|
define_value(constant, default) |
||||
|
logger.warn "#{prefix} restored:#{Semantic::AnsiColors::CLEAR} #{constant} = #{default.ai}" |
||||
|
changes += 1 |
||||
|
end |
||||
|
|
||||
|
return unless changes.positive? |
||||
|
|
||||
|
# TODO: ... |
||||
|
# changes=[] |
||||
|
# changes << {kind: :overriden, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false} |
||||
|
# changes << {kind: :restored, constant: 'ACTION_VIEW', type :boolean, old_value: false, new_value:false} |
||||
|
# ActiveSupport::Notifications.instrument 'rolling.live_constant', this: changes |
||||
|
|
||||
|
return unless RailsLiveReload.watcher |
||||
|
|
||||
|
FileUtils.touch(MAIN_CSS) # triggering RailsLiveReload |
||||
|
end |
||||
|
|
||||
|
private |
||||
|
|
||||
|
def logger = @logger ||= SemanticLogger[self] |
||||
|
def env_values = @env_values ||= Dotenv.parse |
||||
|
def definitions = @definitions ||= {} |
||||
|
|
||||
|
def define_value(constant, value) |
||||
|
definitions[constant][:value] = value |
||||
|
remove_const(constant) |
||||
|
const_set(constant, value) |
||||
|
end |
||||
|
|
||||
|
# origin (or caller[0]) helps fetching the constant name from source code introspection |
||||
|
def define_type_from_callee(origin, type, default) |
||||
|
@@class_origin ||= self # rubocop:disable Style/ClassVars |
||||
|
@listener ||= start_listener |
||||
|
|
||||
|
file, line = origin.split(':') |
||||
|
constant = introspect_constant_from_file(file, line.to_i - 1) |
||||
|
raw_value = env_values.fetch(constant, nil) |
||||
|
value = typed_value(type, raw_value, default) |
||||
|
definitions[constant] = { type:, default:, value: } |
||||
|
|
||||
|
# logger.debug('new definitions', definitions) |
||||
|
value |
||||
|
end |
||||
|
|
||||
|
def start_listener |
||||
|
$definable_thread_group.list.each(&:kill) |
||||
|
$definable_thread_group.add(Thread.new do |
||||
|
listener = Listen.to(Rails.root, only: /^\.env\.?/) do |
||||
|
@@class_origin.reload_from_env |
||||
|
rescue StandardError |
||||
|
nil |
||||
|
end |
||||
|
listener.start |
||||
|
end) |
||||
|
end |
||||
|
|
||||
|
def typed_value(type, raw, default) |
||||
|
return default if raw.nil? |
||||
|
|
||||
|
case type |
||||
|
when :integer then raw.to_i |
||||
|
when :boolean then raw.upcase == 'TRUE' |
||||
|
else raw |
||||
|
end |
||||
|
end |
||||
|
|
||||
|
# returns current directory of this source code |
||||
|
def dir_source_location |
||||
|
return @dir_source_location if defined?(@dir_source_location) |
||||
|
|
||||
|
*paths, _ = Live.const_source_location(:Definable).first.split('/') |
||||
|
@dir_source_location = paths.join('/') |
||||
|
end |
||||
|
|
||||
|
def introspect_constant_from_file(file, line) |
||||
|
*dir_file, _ = file.split('/') |
||||
|
dir_file = dir_file.join('/') |
||||
|
raise "unexpected directory: #{dir_file} != #{dir_source_location}" unless dir_file == dir_source_location |
||||
|
|
||||
|
@lines ||= File.readlines(file) # cached source code |
||||
|
@lines[line].match(/\s*(.\w+)/)[1] # TODO: should be uppercase! |
||||
|
end |
||||
|
end |
||||
|
end |
@ -0,0 +1,17 @@ |
|||||
|
class MonkeyPatcher |
||||
|
class << self |
||||
|
MONKEY_PATCHES = Rails.root.join('lib', 'monkey_patches') |
||||
|
def run(&) |
||||
|
patches = Dir.glob(MONKEY_PATCHES.join('**', '*.rb')) |
||||
|
patches.each do |patch| |
||||
|
file = Pathname.new(patch).relative_path_from(MONKEY_PATCHES).to_s |
||||
|
next if file == 'monkey_patcher.rb' # filter off its own file! |
||||
|
|
||||
|
if !block_given? || yield(file) |
||||
|
puts "🐵 patching... #{file}" |
||||
|
require file |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
||||
|
end |
@ -1,76 +0,0 @@ |
|||||
module Semantic |
|
||||
# use the Zeitwerk autoloader to reattach_appender for development autoreloading feature |
|
||||
class DevLoader |
|
||||
def initialize(session_key) |
|
||||
@session_key = session_key |
|
||||
@subscribers = {} |
|
||||
|
|
||||
RailsSemanticLogger::ActionController::LogSubscriber.logger.level = :fatal # useful for remanent Rack::Log started |
|
||||
|
|
||||
launch |
|
||||
end |
|
||||
|
|
||||
def launch |
|
||||
once_and_reload do |
|
||||
append_ansi_formatter |
|
||||
|
|
||||
Semantic::NotificationUtil.clear_subscribers(/\.action_controller$/) |
|
||||
Semantic::NotificationUtil.clear_subscribers(/\.action_view$/) |
|
||||
reset_subscribers |
|
||||
|
|
||||
register_action_controller |
|
||||
register_action_view |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
private |
|
||||
|
|
||||
def once_and_reload(&) |
|
||||
yield |
|
||||
Rails.autoloaders.main.on_load('ApplicationController', &) |
|
||||
end |
|
||||
|
|
||||
def append_ansi_formatter |
|
||||
SemanticLogger.clear_appenders! |
|
||||
formatter = Semantic::AnsiFormatter.new |
|
||||
SemanticLogger.add_appender(io: $stdout, |
|
||||
formatter:, |
|
||||
filter: ->(log) { !formatter.reject(log) }) |
|
||||
end |
|
||||
|
|
||||
def register_action_controller |
|
||||
sub_instance = Semantic::Subscribers::ActionController.new(@session_key) |
|
||||
register_hook(sub_instance, :start_processing) |
|
||||
register_hook(sub_instance, :process_action, :finish_processing) |
|
||||
register_hook(sub_instance, :redirect_to) |
|
||||
%i[send_file send_data halted_callback unpermitted_parameters send_stream write_fragment |
|
||||
read_fragment expire_fragment exist_fragment?].each do |hook| |
|
||||
register_hook(sub_instance, hook, :any_hook) |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
def register_action_view |
|
||||
sub_instance = Semantic::Subscribers::ActionView.new |
|
||||
%i[render_template render_partial render_collection render_layout].each do |hook| |
|
||||
register_hook(sub_instance, hook) |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
def register_hook(sub_instance, hook, method = hook) |
|
||||
@subscribers[sub_instance.class] ||= [] |
|
||||
@subscribers[sub_instance.class] << ActiveSupport::Notifications.subscribe("#{hook}.#{sub_instance.event_group}") do |event| |
|
||||
sub_instance.send(method, event) |
|
||||
end |
|
||||
end |
|
||||
|
|
||||
def reset_subscribers |
|
||||
return if @subscribers.empty? |
|
||||
|
|
||||
@subscribers.each_pair do |clazz, subs| |
|
||||
# puts "reset #{subs.size} subscribers for class <#{clazz}>" |
|
||||
subs.each { |sub| ActiveSupport::Notifications.unsubscribe(sub) } |
|
||||
subs.clear |
|
||||
end |
|
||||
end |
|
||||
end |
|
||||
end |
|
@ -1,39 +0,0 @@ |
|||||
module Semantic |
|
||||
module NotificationUtil |
|
||||
class << self |
|
||||
# pattern could be either a string 'start_processing.action_controller' or a regex /\.action_controller$/ |
|
||||
# FIXME: weird behaviour, order impact!!!! |
|
||||
# For instance: |
|
||||
# OK |
|
||||
# NotificationUtil.clear_subscribers(/\.action_controller$/) |
|
||||
# NotificationUtil.clear_subscribers(/\.action_view$/) |
|
||||
# NOPE |
|
||||
# NotificationUtil.clear_subscribers(/\.action_view$/) |
|
||||
# NotificationUtil.clear_subscribers(/\.action_controller$/) |
|
||||
def clear_subscribers(pattern) |
|
||||
ActiveSupport::LogSubscriber.subscribers.each { |sub| unattach(sub, pattern) } |
|
||||
end |
|
||||
|
|
||||
private |
|
||||
|
|
||||
def subscriber_patterns(subscriber) |
|
||||
subscriber.patterns.respond_to?(:keys) ? subscriber.patterns.keys : subscriber.patterns |
|
||||
end |
|
||||
|
|
||||
def unattach(subscriber, pattern) |
|
||||
subscriber_patterns(subscriber).each do |sub_pattern| |
|
||||
ActiveSupport::Notifications.notifier.listeners_for(sub_pattern).each do |sub| |
|
||||
next unless sub.instance_variable_get(:@delegate) == subscriber |
|
||||
next unless pattern.match(sub_pattern) |
|
||||
|
|
||||
puts "FOUND subscriber=#{subscriber} for sub_pattern=#{sub_pattern} with logger #{subscriber.logger.name}" |
|
||||
puts subscriber.class.module_parent.const_source_location(subscriber.class.to_s)&.first |
|
||||
|
|
||||
ActiveSupport::Notifications.unsubscribe(sub) |
|
||||
end |
|
||||
end |
|
||||
# ActiveSupport::LogSubscriber.subscribers.delete(subscriber) |
|
||||
end |
|
||||
end |
|
||||
end |
|
||||
end |
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue