funicular 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +56 -1
- data/README.md +58 -20
- data/Rakefile +74 -2
- data/demo/keymap_editor.html +582 -0
- data/demo/test_cable.html +179 -0
- data/demo/test_chartjs.html +235 -0
- data/demo/test_component.html +201 -0
- data/demo/test_diff_patch.html +146 -0
- data/demo/test_error_boundary.html +284 -0
- data/demo/test_router.html +257 -0
- data/demo/test_vdom.html +100 -0
- data/demo/tic-tac-toe.html +201 -0
- data/docs/README.md +419 -0
- data/docs/advanced-features.md +632 -0
- data/docs/architecture.md +409 -0
- data/docs/components-and-state.md +539 -0
- data/docs/data-fetching.md +528 -0
- data/docs/forms.md +446 -0
- data/docs/rails-integration.md +426 -0
- data/docs/realtime.md +543 -0
- data/docs/routing-and-navigation.md +427 -0
- data/docs/styling.md +285 -0
- data/exe/funicular +32 -0
- data/lib/funicular/assets/funicular.rb +21 -0
- data/lib/funicular/assets/funicular_debug.css +73 -0
- data/lib/funicular/assets/funicular_debug.js +183 -0
- data/lib/funicular/commands/routes.rb +69 -0
- data/lib/funicular/compiler.rb +135 -0
- data/lib/funicular/configuration.rb +76 -0
- data/lib/funicular/helpers/picoruby_helper.rb +50 -0
- data/lib/funicular/middleware.rb +98 -0
- data/lib/funicular/railtie.rb +26 -0
- data/lib/funicular/route_parser.rb +137 -0
- data/lib/funicular/vendor/picorbc/VERSION +1 -0
- data/lib/funicular/vendor/picorbc/picorbc.js +5283 -0
- data/lib/funicular/vendor/picorbc/picorbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -0
- data/lib/funicular/vendor/picoruby/debug/init.iife.js +130 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +6404 -0
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/init.iife.js +130 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +29 -1
- data/lib/tasks/funicular.rake +135 -0
- data/minitest/funicular_test.rb +13 -0
- data/minitest/test_helper.rb +7 -0
- data/mrbgem.rake +15 -0
- data/mrblib/cable.rb +417 -0
- data/mrblib/component.rb +911 -0
- data/mrblib/debug.rb +205 -0
- data/mrblib/differ.rb +244 -0
- data/mrblib/environment_inquirer.rb +34 -0
- data/mrblib/error_boundary.rb +125 -0
- data/mrblib/file_upload.rb +184 -0
- data/mrblib/form_builder.rb +284 -0
- data/mrblib/funicular.rb +156 -0
- data/mrblib/http.rb +89 -0
- data/mrblib/model.rb +146 -0
- data/mrblib/patcher.rb +203 -0
- data/mrblib/router.rb +229 -0
- data/mrblib/styles.rb +83 -0
- data/mrblib/vdom.rb +273 -0
- data/sig/cable.rbs +65 -0
- data/sig/component.rbs +141 -0
- data/sig/debug.rbs +28 -0
- data/sig/differ.rbs +18 -0
- data/sig/environment_iquirer.rbs +10 -0
- data/sig/error_boundary.rbs +14 -0
- data/sig/file_upload.rbs +18 -0
- data/sig/form_builder.rbs +29 -0
- data/sig/funicular.rbs +11 -1
- data/sig/http.rbs +22 -0
- data/sig/model.rbs +23 -0
- data/sig/patcher.rbs +15 -0
- data/sig/router.rbs +43 -0
- data/sig/styles.rbs +25 -0
- data/sig/vdom.rbs +59 -0
- metadata +119 -8
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Funicular
|
|
4
|
+
class Middleware
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :last_mtime, :compiling, :mutex
|
|
7
|
+
|
|
8
|
+
def reset!
|
|
9
|
+
@last_mtime = nil
|
|
10
|
+
@compiling = false
|
|
11
|
+
@mutex = Mutex.new
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Initialize class state
|
|
16
|
+
reset!
|
|
17
|
+
|
|
18
|
+
def initialize(app)
|
|
19
|
+
@app = app
|
|
20
|
+
@source_dir = Rails.root.join("app", "funicular")
|
|
21
|
+
@output_file = Rails.root.join("app", "assets", "builds", "app.mrb")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(env)
|
|
25
|
+
recompile_if_needed if should_check_recompile?
|
|
26
|
+
@app.call(env)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def should_check_recompile?
|
|
32
|
+
Rails.env.development? && Dir.exist?(@source_dir)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def recompile_if_needed
|
|
36
|
+
current_mtime = latest_source_mtime
|
|
37
|
+
|
|
38
|
+
# Skip if already compiling or if no changes detected
|
|
39
|
+
return if self.class.compiling
|
|
40
|
+
return if self.class.last_mtime && current_mtime <= self.class.last_mtime
|
|
41
|
+
|
|
42
|
+
self.class.mutex.synchronize do
|
|
43
|
+
# Double-check inside the lock
|
|
44
|
+
return if self.class.compiling
|
|
45
|
+
return if self.class.last_mtime && current_mtime <= self.class.last_mtime
|
|
46
|
+
|
|
47
|
+
self.class.compiling = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
begin
|
|
51
|
+
Rails.logger.info "Funicular: Source files changed, recompiling..."
|
|
52
|
+
compiler = Compiler.new(
|
|
53
|
+
source_dir: @source_dir,
|
|
54
|
+
output_file: @output_file,
|
|
55
|
+
debug_mode: true,
|
|
56
|
+
logger: Rails.logger
|
|
57
|
+
)
|
|
58
|
+
compiler.compile
|
|
59
|
+
self.class.last_mtime = current_mtime
|
|
60
|
+
invalidate_asset_pipeline_cache
|
|
61
|
+
rescue => e
|
|
62
|
+
Rails.logger.error "Funicular compilation failed: #{e.message}"
|
|
63
|
+
ensure
|
|
64
|
+
self.class.compiling = false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Force the asset pipeline to drop its cached fingerprint for app.mrb.
|
|
69
|
+
#
|
|
70
|
+
# Propshaft caches Asset instances (and memoizes #digest / #compiled_content)
|
|
71
|
+
# in LoadPath, and only refreshes them when its file watcher detects a change
|
|
72
|
+
# in a file whose extension is registered in Mime::EXTENSION_LOOKUP. The .mrb
|
|
73
|
+
# extension is not registered there, so when funicular rewrites app.mrb the
|
|
74
|
+
# Propshaft cache is never invalidated and asset_path('app.mrb') keeps
|
|
75
|
+
# returning the stale fingerprinted URL until the Rails process is restarted.
|
|
76
|
+
#
|
|
77
|
+
# We side-step that by invoking the cache sweeper directly after every
|
|
78
|
+
# successful recompile. This is a no-op if Propshaft is not in use.
|
|
79
|
+
def invalidate_asset_pipeline_cache
|
|
80
|
+
return unless Rails.application.respond_to?(:assets)
|
|
81
|
+
|
|
82
|
+
assets = Rails.application.assets
|
|
83
|
+
return unless assets.respond_to?(:load_path)
|
|
84
|
+
|
|
85
|
+
load_path = assets.load_path
|
|
86
|
+
return unless load_path.respond_to?(:cache_sweeper)
|
|
87
|
+
|
|
88
|
+
load_path.cache_sweeper.execute
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def latest_source_mtime
|
|
92
|
+
source_files = Dir.glob(File.join(@source_dir, "**", "*.rb"))
|
|
93
|
+
return Time.at(0) if source_files.empty?
|
|
94
|
+
|
|
95
|
+
source_files.map { |f| File.mtime(f) }.max
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module Funicular
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
railtie_name :funicular
|
|
8
|
+
|
|
9
|
+
initializer "funicular.middleware" do |app|
|
|
10
|
+
if Rails.env.development?
|
|
11
|
+
app.middleware.use Funicular::Middleware
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
initializer "funicular.helpers" do
|
|
16
|
+
ActiveSupport.on_load(:action_view) do
|
|
17
|
+
require "funicular/helpers/picoruby_helper"
|
|
18
|
+
include Funicular::Helpers::PicorubyHelper
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rake_tasks do
|
|
23
|
+
load "tasks/funicular.rake"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Funicular
|
|
4
|
+
class RouteParser
|
|
5
|
+
attr_reader :routes
|
|
6
|
+
|
|
7
|
+
def initialize(source_file)
|
|
8
|
+
@source_file = source_file
|
|
9
|
+
@routes = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse
|
|
13
|
+
return [] unless File.exist?(@source_file)
|
|
14
|
+
|
|
15
|
+
content = File.read(@source_file)
|
|
16
|
+
lines = content.split("\n")
|
|
17
|
+
|
|
18
|
+
lines.each do |line|
|
|
19
|
+
# Skip comments and empty lines
|
|
20
|
+
trimmed = line.strip
|
|
21
|
+
next if trimmed.empty? || trimmed.start_with?('#')
|
|
22
|
+
|
|
23
|
+
# Parse router.get/post/put/patch/delete lines
|
|
24
|
+
if trimmed.include?('router.')
|
|
25
|
+
parse_route_line(trimmed)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@routes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def parse_route_line(line)
|
|
35
|
+
# Extract HTTP method
|
|
36
|
+
method = nil
|
|
37
|
+
['get', 'post', 'put', 'patch', 'delete', 'add_route'].each do |m|
|
|
38
|
+
if line.include?("router.#{m}")
|
|
39
|
+
method = m
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return unless method
|
|
45
|
+
|
|
46
|
+
# Extract path (between first pair of quotes)
|
|
47
|
+
path = extract_quoted_string(line)
|
|
48
|
+
return unless path
|
|
49
|
+
|
|
50
|
+
# Extract component name (after 'to:' or as second argument)
|
|
51
|
+
component = nil
|
|
52
|
+
if line.include?('to:')
|
|
53
|
+
to_idx = line.index('to:')
|
|
54
|
+
if to_idx
|
|
55
|
+
# Component name is after 'to:'
|
|
56
|
+
after_to = line[to_idx + 3..-1].strip
|
|
57
|
+
# Find where component name ends (comma or paren)
|
|
58
|
+
end_idx = find_first_of(after_to, [',', ')'])
|
|
59
|
+
if end_idx
|
|
60
|
+
component = after_to[0...end_idx].strip
|
|
61
|
+
else
|
|
62
|
+
component = after_to.strip
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
elsif method == 'add_route'
|
|
66
|
+
# Old style: router.add_route('/path', ComponentName)
|
|
67
|
+
# Find second comma-separated value
|
|
68
|
+
first_comma = line.index(',')
|
|
69
|
+
if first_comma
|
|
70
|
+
after_comma = line[first_comma + 1..-1].strip
|
|
71
|
+
# Component name ends at comma or paren
|
|
72
|
+
end_idx = find_first_of(after_comma, [',', ')'])
|
|
73
|
+
if end_idx
|
|
74
|
+
component = after_comma[0...end_idx].strip
|
|
75
|
+
else
|
|
76
|
+
component = after_comma.strip
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
return unless component
|
|
82
|
+
|
|
83
|
+
# Extract helper name (after 'as:')
|
|
84
|
+
helper_name = nil
|
|
85
|
+
if line.include?('as:')
|
|
86
|
+
as_idx = line.index('as:')
|
|
87
|
+
if as_idx
|
|
88
|
+
after_as = line[as_idx + 3..-1]
|
|
89
|
+
helper_str = extract_quoted_string(after_as)
|
|
90
|
+
helper_name = helper_str ? "#{helper_str}_path" : nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Add route
|
|
95
|
+
@routes << {
|
|
96
|
+
method: method == 'add_route' ? 'GET' : method.upcase,
|
|
97
|
+
path: path,
|
|
98
|
+
component: component,
|
|
99
|
+
helper: helper_name
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def extract_quoted_string(text)
|
|
104
|
+
# Find first quoted string (single or double quotes)
|
|
105
|
+
start_idx = nil
|
|
106
|
+
quote_char = nil
|
|
107
|
+
|
|
108
|
+
text.each_char.with_index do |char, idx|
|
|
109
|
+
if char == '"' || char == "'"
|
|
110
|
+
if start_idx.nil?
|
|
111
|
+
start_idx = idx
|
|
112
|
+
quote_char = char
|
|
113
|
+
elsif char == quote_char
|
|
114
|
+
# Found closing quote
|
|
115
|
+
return text[(start_idx + 1)...idx]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def find_first_of(text, chars)
|
|
124
|
+
# Find index of first occurrence of any char in chars array
|
|
125
|
+
min_idx = nil
|
|
126
|
+
|
|
127
|
+
chars.each do |char|
|
|
128
|
+
idx = text.index(char)
|
|
129
|
+
if idx
|
|
130
|
+
min_idx = idx if min_idx.nil? || idx < min_idx
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
min_idx
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.0
|