funicular 0.3.0 → 0.5.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 +486 -1
- data/demo/local_notes.html +207 -0
- data/demo/test_chartjs.html +9 -9
- data/demo/test_component.html +8 -8
- data/demo/test_error_boundary.html +44 -41
- data/demo/test_router.html +48 -48
- data/demo/tic-tac-toe.html +25 -25
- data/docs/architecture.md +227 -12
- data/docs/local_database.md +1035 -0
- data/lib/funicular/assets/funicular.rb +14 -0
- data/lib/funicular/configuration.rb +65 -0
- data/lib/funicular/epoch_header.rb +69 -0
- data/lib/funicular/epoch_stamping.rb +66 -0
- data/lib/funicular/helpers/picoruby_helper.rb +96 -1
- data/lib/funicular/railtie.rb +30 -0
- data/lib/funicular/schema.rb +45 -12
- data/lib/funicular/session_epoch.rb +110 -0
- data/lib/funicular/ssr/runtime.rb +58 -12
- data/lib/funicular/ssr.rb +25 -0
- data/lib/funicular/testing/node_runner.mjs +19 -0
- data/lib/funicular/testing.rb +47 -0
- data/lib/funicular/vendor/mrbc/VERSION +1 -1
- data/lib/funicular/vendor/mrbc/mrbc.js +655 -574
- data/lib/funicular/vendor/mrbc/mrbc.wasm +0 -0
- data/lib/funicular/vendor/picoruby/VERSION +1 -1
- data/lib/funicular/vendor/picoruby/debug/picoruby.js +800 -530
- data/lib/funicular/vendor/picoruby/debug/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby/dist/picoruby.js +2 -2
- data/lib/funicular/vendor/picoruby/dist/picoruby.wasm +0 -0
- data/lib/funicular/vendor/picoruby-test-node/VERSION +1 -1
- data/lib/funicular/vendor/picoruby-test-node/picoruby.js +2 -6909
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm +0 -0
- data/lib/funicular/version.rb +1 -1
- data/lib/funicular.rb +1 -0
- data/lib/generators/funicular/chat/templates/funicular_chat_component.rb.tt +37 -38
- data/lib/tasks/funicular.rake +10 -2
- data/minitest/callback_error_visibility_test.rb +48 -0
- data/minitest/configuration_test.rb +78 -0
- data/minitest/dsl_test.rb +264 -0
- data/minitest/epoch_header_test.rb +149 -0
- data/minitest/epoch_stamping_test.rb +225 -0
- data/minitest/fixtures/funicular_app/components/greeting_component.rb +5 -5
- data/minitest/fixtures/funicular_app/components/probe_component.rb +15 -0
- data/minitest/form_for_test.rb +2 -2
- data/minitest/hydration_test.rb +2 -2
- data/minitest/navigation_guard_test.rb +65 -0
- data/minitest/picoruby_helper_test.rb +236 -0
- data/minitest/schema_test.rb +47 -0
- data/minitest/session_epoch_test.rb +122 -0
- data/minitest/sig_tags_test.rb +30 -0
- data/minitest/ssr_database_test.rb +78 -0
- data/minitest/ssr_reload_test.rb +106 -0
- data/minitest/ssr_test.rb +41 -0
- data/minitest/testing_ensure_compiled_test.rb +52 -0
- data/minitest/validations_test.rb +35 -5
- data/minitest/view_context_test.rb +15 -15
- data/mrbgem.rake +2 -0
- data/mrblib/0_tags.rb +62 -0
- data/mrblib/cable.rb +1 -1
- data/mrblib/component.rb +226 -24
- data/mrblib/db.rb +3116 -0
- data/mrblib/error_boundary.rb +25 -19
- data/mrblib/file_upload.rb +17 -7
- data/mrblib/form_builder.rb +10 -10
- data/mrblib/funicular.rb +136 -17
- data/mrblib/http.rb +84 -107
- data/mrblib/model.rb +1178 -23
- data/mrblib/relation.rb +342 -0
- data/mrblib/router.rb +45 -4
- data/mrblib/styles.rb +122 -12
- data/mrblib/view_context.rb +3 -32
- data/sig/component.rbs +25 -4
- data/sig/db.rbs +328 -0
- data/sig/error_boundary.rbs +4 -4
- data/sig/funicular.rbs +5 -0
- data/sig/http.rbs +8 -21
- data/sig/model.rbs +101 -7
- data/sig/relation.rbs +44 -0
- data/sig/router.rbs +1 -0
- data/sig/styles.rbs +19 -5
- data/sig/tags.rbs +54 -0
- data/sig/view_context.rbs +47 -34
- metadata +23 -2
- data/lib/funicular/vendor/picoruby-test-node/picoruby.wasm.map +0 -1
|
@@ -15,12 +15,14 @@ module Funicular
|
|
|
15
15
|
# (Funicular.start, Router#start, FileUpload.mount, Debug) no-ops.
|
|
16
16
|
module Runtime
|
|
17
17
|
MRBLIB_DIR = File.expand_path("../../../mrblib", __dir__)
|
|
18
|
+
BOOT_MUTEX = Mutex.new
|
|
18
19
|
|
|
19
20
|
# Load order is by dependency at *class-body* evaluation time. Most
|
|
20
21
|
# files reference JS / other classes only inside methods, so only a
|
|
21
22
|
# few real dependencies exist (vdom before html_serializer; styles and
|
|
22
23
|
# vdom before component; component before error_boundary).
|
|
23
24
|
LOAD_ORDER = %w[
|
|
25
|
+
0_tags
|
|
24
26
|
environment_inquirer
|
|
25
27
|
vdom
|
|
26
28
|
html_serializer
|
|
@@ -35,6 +37,8 @@ module Funicular
|
|
|
35
37
|
router
|
|
36
38
|
0_validations
|
|
37
39
|
1_validators
|
|
40
|
+
db
|
|
41
|
+
relation
|
|
38
42
|
model
|
|
39
43
|
store
|
|
40
44
|
store_singleton
|
|
@@ -70,33 +74,75 @@ module Funicular
|
|
|
70
74
|
# In that case we rescue and continue: the AR constant is already defined
|
|
71
75
|
# and is all that load_schemas needs (it ignores the hash on the server).
|
|
72
76
|
#
|
|
73
|
-
# Loaded once per process.
|
|
77
|
+
# Loaded once per process. With auto_reload (the railtie enables
|
|
78
|
+
# it in development), edited sources are picked up on the next
|
|
79
|
+
# render: reloading re-runs the initializer, and the server-side
|
|
80
|
+
# Funicular.start builds a fresh router, so routes refresh too.
|
|
81
|
+
# Without it, restart the server to pick up changes.
|
|
74
82
|
def boot!(source_dir)
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
# Fast path for the steady production/test state: everything
|
|
84
|
+
# loaded and nobody watching for changes, no lock to take.
|
|
85
|
+
return if @framework_loaded && @app_loaded && !auto_reload
|
|
77
86
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
warn "[Funicular SSR] Skipped #{File.basename(file)}: #{e.message}"
|
|
87
|
+
# Concurrent renders (Puma threads in development) must not
|
|
88
|
+
# interleave two reloads: constants and the router would pass
|
|
89
|
+
# through inconsistent states mid-request.
|
|
90
|
+
BOOT_MUTEX.synchronize do
|
|
91
|
+
load_framework!
|
|
92
|
+
if @app_loaded
|
|
93
|
+
next unless auto_reload && sources_changed?(source_dir)
|
|
86
94
|
end
|
|
95
|
+
|
|
96
|
+
files = Funicular::Compiler.source_files(source_dir.to_s)
|
|
97
|
+
files.each do |file|
|
|
98
|
+
begin
|
|
99
|
+
Kernel.load(file)
|
|
100
|
+
rescue TypeError => e
|
|
101
|
+
# Funicular model name conflicts with an already-loaded AR model.
|
|
102
|
+
# The constant is already defined; safe to skip.
|
|
103
|
+
warn "[Funicular SSR] Skipped #{File.basename(file)}: #{e.message}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
@app_loaded = true
|
|
107
|
+
@sources_snapshot = sources_snapshot(source_dir)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Reload edited app sources on the next boot! instead of
|
|
112
|
+
# requiring a server restart (meant for development).
|
|
113
|
+
attr_accessor :auto_reload
|
|
114
|
+
|
|
115
|
+
def sources_changed?(source_dir)
|
|
116
|
+
@sources_snapshot != sources_snapshot(source_dir)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def sources_snapshot(source_dir)
|
|
120
|
+
Funicular::Compiler.source_files(source_dir.to_s).map do |file|
|
|
121
|
+
[ file, mtime_or_nil(file) ]
|
|
87
122
|
end
|
|
88
|
-
@app_loaded = true
|
|
89
123
|
end
|
|
90
124
|
|
|
91
125
|
# Test/escape hatch: forget loaded application state so a different
|
|
92
126
|
# app (or a reload) can be booted. Does not unload the framework.
|
|
93
127
|
def reset_app!
|
|
94
128
|
@app_loaded = false
|
|
129
|
+
@sources_snapshot = nil
|
|
95
130
|
end
|
|
96
131
|
|
|
97
132
|
def framework_loaded?
|
|
98
133
|
!!@framework_loaded
|
|
99
134
|
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
# The mtime read races with editors deleting or renaming files
|
|
139
|
+
# mid-edit; a vanished file counts as nil instead of breaking
|
|
140
|
+
# the render.
|
|
141
|
+
def mtime_or_nil(file)
|
|
142
|
+
File.mtime(file).to_f
|
|
143
|
+
rescue SystemCallError
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
100
146
|
end
|
|
101
147
|
end
|
|
102
148
|
end
|
data/lib/funicular/ssr.rb
CHANGED
|
@@ -37,6 +37,31 @@ module Funicular
|
|
|
37
37
|
{ html: html, state: state, component: component_class }
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Render one component to an HTML string with no route lookup: for
|
|
41
|
+
# embedding a Funicular component into an otherwise server-rendered
|
|
42
|
+
# (ERB) page -- a shared site header, say -- so the markup has a
|
|
43
|
+
# single source of truth. The component is named by string and
|
|
44
|
+
# resolved after boot!, because host apps keep app/funicular out of
|
|
45
|
+
# Rails autoloading and the constant does not exist before loading.
|
|
46
|
+
# Handlers are not bound and no hydration happens: the output is
|
|
47
|
+
# static HTML (links still work; onclick and friends do not).
|
|
48
|
+
def self.render_component(component_name, props: {}, state: {}, source_dir: nil)
|
|
49
|
+
Runtime.boot!(source_dir || default_source_dir)
|
|
50
|
+
|
|
51
|
+
router = Funicular.router
|
|
52
|
+
raise "Funicular router is not configured; check app/funicular/initializer.rb" unless router
|
|
53
|
+
|
|
54
|
+
component_class = Object.const_get(component_name.to_s)
|
|
55
|
+
unless component_class.is_a?(Class) && component_class < Funicular::Component
|
|
56
|
+
raise ArgumentError, "#{component_name} is not a Funicular::Component subclass"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
instance = component_class.new(symbolize_keys(props))
|
|
60
|
+
instance.runtime = Funicular::Runtime.new(router)
|
|
61
|
+
instance.seed_state(state)
|
|
62
|
+
Funicular::VDOM::HTMLSerializer.serialize(instance.build_vdom, instance.runtime)
|
|
63
|
+
end
|
|
64
|
+
|
|
40
65
|
def self.default_source_dir
|
|
41
66
|
raise "source_dir is required outside Rails" unless defined?(Rails) && Rails.respond_to?(:root)
|
|
42
67
|
Rails.root.join("app", "funicular")
|
|
@@ -105,6 +105,15 @@ module Funicular
|
|
|
105
105
|
report(!actual.nil?, "Expected selector #{selector.inspect} to exist", selector, actual)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
def assert_no_selector(selector)
|
|
109
|
+
actual = query(selector)
|
|
110
|
+
report(actual.nil?, "Expected selector #{selector.inspect} to be absent", selector, actual)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def selector_count(selector)
|
|
114
|
+
JS.eval("document.querySelectorAll(" + JSON.generate(selector) + ").length").to_i
|
|
115
|
+
end
|
|
116
|
+
|
|
108
117
|
def text(selector = nil)
|
|
109
118
|
target = selector ? query(selector) : document.body
|
|
110
119
|
target ? target[:textContent].to_s : ''
|
|
@@ -120,6 +129,16 @@ module Funicular
|
|
|
120
129
|
)
|
|
121
130
|
end
|
|
122
131
|
|
|
132
|
+
def assert_no_text(expected, selector = nil)
|
|
133
|
+
actual = text(selector)
|
|
134
|
+
report(
|
|
135
|
+
!actual.include?(expected.to_s),
|
|
136
|
+
"Expected text not to include #{expected.to_s.inspect}",
|
|
137
|
+
expected.to_s,
|
|
138
|
+
actual
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
123
142
|
def dispatch(selector, event_type)
|
|
124
143
|
script = "document.querySelector(" + JSON.generate(selector) + ")" \
|
|
125
144
|
+ ".dispatchEvent(new Event(" + JSON.generate(event_type) \
|
data/lib/funicular/testing.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "testing/node_runner"
|
|
4
|
+
require_relative "plugin"
|
|
5
|
+
require_relative "compiler"
|
|
4
6
|
|
|
5
7
|
module Funicular
|
|
6
8
|
module Testing
|
|
@@ -8,6 +10,51 @@ module Funicular
|
|
|
8
10
|
NodeRunner.new(**options).run
|
|
9
11
|
end
|
|
10
12
|
|
|
13
|
+
# One-call test-environment setup: syncs plugin assets and compiles
|
|
14
|
+
# the client bundle when it is missing or older than any source or
|
|
15
|
+
# plugin file. Development recompiles through the middleware and
|
|
16
|
+
# production through assets:precompile, but the test environment
|
|
17
|
+
# serves whatever app.mrb is lying around; call this once from
|
|
18
|
+
# test_helper and rely on the mtime check to keep it cheap.
|
|
19
|
+
#
|
|
20
|
+
# Funicular::Testing.ensure_compiled! # Rails.root
|
|
21
|
+
# Funicular::Testing.ensure_compiled!(force: true)
|
|
22
|
+
#
|
|
23
|
+
# Returns the path of the compiled bundle.
|
|
24
|
+
def self.ensure_compiled!(root: nil, force: false, debug_mode: true)
|
|
25
|
+
root = Pathname.new(root || default_root)
|
|
26
|
+
registry = Funicular::Plugin::Registry.new(root)
|
|
27
|
+
registry.validate!
|
|
28
|
+
registry.sync_assets
|
|
29
|
+
|
|
30
|
+
source_dir = root.join("app", "funicular").to_s
|
|
31
|
+
output_file = root.join("app", "assets", "builds", "app.mrb").to_s
|
|
32
|
+
sources = registry.local_source_files + Funicular::Compiler.source_files(source_dir)
|
|
33
|
+
|
|
34
|
+
if force || stale?(output_file, sources)
|
|
35
|
+
Funicular::Compiler.new(
|
|
36
|
+
source_dir: source_dir,
|
|
37
|
+
output_file: output_file,
|
|
38
|
+
debug_mode: debug_mode,
|
|
39
|
+
prepend_source_files: registry.local_source_files
|
|
40
|
+
).compile
|
|
41
|
+
end
|
|
42
|
+
output_file
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A bundle is stale when it does not exist or any source file was
|
|
46
|
+
# modified after it was built.
|
|
47
|
+
def self.stale?(output_file, source_files)
|
|
48
|
+
return true unless File.exist?(output_file)
|
|
49
|
+
build_time = File.mtime(output_file)
|
|
50
|
+
source_files.any? { |f| File.exist?(f) && File.mtime(f) > build_time }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.default_root
|
|
54
|
+
raise ArgumentError, "root is required outside Rails" unless defined?(Rails) && Rails.respond_to?(:root)
|
|
55
|
+
Rails.root
|
|
56
|
+
end
|
|
57
|
+
|
|
11
58
|
def self.assert_picotests(test_case, result, print_summary: true)
|
|
12
59
|
puts result.picotest_summary if print_summary
|
|
13
60
|
test_case.assert result.success?, result.output
|
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.3
|