ruact 0.0.4 → 0.0.6
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/.github/workflows/ci.yml +54 -2
- data/.rubocop_todo.yml +3 -115
- data/CHANGELOG.md +68 -17
- data/bench/server_functions_dispatch_bench.rb +109 -142
- data/bench/server_functions_dispatch_bench.results.md +29 -0
- data/docs/internal/decisions/server-functions-api.md +402 -0
- data/lib/generators/ruact/install/install_generator.rb +310 -25
- data/lib/generators/ruact/install/templates/Procfile.dev.tt +2 -0
- data/lib/generators/ruact/install/templates/dev.tt +16 -0
- data/lib/generators/ruact/install/templates/package.json.tt +17 -0
- data/lib/generators/ruact/install/templates/vite.config.js.tt +3 -1
- data/lib/generators/ruact/scaffold/scaffold_attribute.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_form_helpers.rb +114 -0
- data/lib/generators/ruact/scaffold/scaffold_generator.rb +491 -0
- data/lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb +229 -0
- data/lib/generators/ruact/scaffold/templates/components/DeleteDialog.tsx.tt +104 -0
- data/lib/generators/ruact/scaffold/templates/components/Form.tsx.tt +212 -0
- data/lib/generators/ruact/scaffold/templates/components/List.tsx.tt +338 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/DeleteDialog.tsx.tt +92 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/Form.tsx.tt +174 -0
- data/lib/generators/ruact/scaffold/templates/components/agnostic/List.tsx.tt +253 -0
- data/lib/generators/ruact/scaffold/templates/controller.rb.tt +130 -0
- data/lib/generators/ruact/scaffold/templates/queries/application_query.rb.tt +13 -0
- data/lib/generators/ruact/scaffold/templates/queries/query.rb.tt +59 -0
- data/lib/generators/ruact/scaffold/templates/request_spec.rb.tt +77 -0
- data/lib/generators/ruact/scaffold/templates/views/edit.html.erb.tt +7 -0
- data/lib/generators/ruact/scaffold/templates/views/index.html.erb.tt +6 -0
- data/lib/generators/ruact/scaffold/templates/views/new.html.erb.tt +5 -0
- data/lib/generators/ruact/scaffold/templates/views/show.html.erb.tt +16 -0
- data/lib/ruact/client_manifest.rb +37 -36
- data/lib/ruact/component_contract.rb +115 -0
- data/lib/ruact/configuration.rb +80 -28
- data/lib/ruact/controller.rb +69 -421
- data/lib/ruact/doctor.rb +133 -4
- data/lib/ruact/erb_preprocessor.rb +65 -12
- data/lib/ruact/erb_preprocessor_hook.rb +4 -1
- data/lib/ruact/errors.rb +30 -44
- data/lib/ruact/html_converter.rb +22 -1
- data/lib/ruact/railtie.rb +56 -200
- data/lib/ruact/server.rb +28 -6
- data/lib/ruact/server_functions/codegen.rb +49 -188
- data/lib/ruact/server_functions/codegen_v2.rb +23 -6
- data/lib/ruact/server_functions/codegen_v2_query_params.rb +140 -0
- data/lib/ruact/server_functions/error_payload.rb +1 -1
- data/lib/ruact/server_functions/error_rendering.rb +22 -29
- data/lib/ruact/server_functions/name_bridge.rb +14 -16
- data/lib/ruact/server_functions/query_source.rb +35 -8
- data/lib/ruact/server_functions/route_source.rb +3 -4
- data/lib/ruact/server_functions/snapshot.rb +12 -139
- data/lib/ruact/server_functions/validation_errors.rb +70 -0
- data/lib/ruact/server_functions.rb +21 -25
- data/lib/ruact/signed_references.rb +162 -0
- data/lib/ruact/string_distance.rb +72 -0
- data/lib/ruact/validation_errors_collector.rb +139 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +102 -0
- data/lib/ruact.rb +19 -19
- data/lib/tasks/ruact.rake +10 -53
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/errors_demo/new.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +36 -0
- data/spec/ruact/component_contract_spec.rb +119 -0
- data/spec/ruact/configuration_spec.rb +51 -34
- data/spec/ruact/controller_request_spec.rb +264 -0
- data/spec/ruact/controller_spec.rb +63 -326
- data/spec/ruact/doctor_spec.rb +201 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +4 -1
- data/spec/ruact/erb_preprocessor_spec.rb +127 -0
- data/spec/ruact/errors_spec.rb +0 -45
- data/spec/ruact/html_converter_spec.rb +50 -0
- data/spec/ruact/install_generator_spec.rb +591 -4
- data/spec/ruact/query_request_spec.rb +109 -1
- data/spec/ruact/scaffold_generator_spec.rb +1835 -0
- data/spec/ruact/server_bucket_request_spec.rb +142 -0
- data/spec/ruact/server_function_name_spec.rb +1 -1
- data/spec/ruact/server_functions/codegen_spec.rb +158 -269
- data/spec/ruact/server_functions/name_bridge_spec.rb +9 -9
- data/spec/ruact/server_functions/query_source_spec.rb +51 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +71 -268
- data/spec/ruact/server_functions/rake_spec.rb +29 -29
- data/spec/ruact/server_functions/snapshot_spec.rb +53 -213
- data/spec/ruact/server_rescue_request_spec.rb +6 -6
- data/spec/ruact/server_spec.rb +8 -9
- data/spec/ruact/signed_references_spec.rb +164 -0
- data/spec/ruact/string_distance_spec.rb +38 -0
- data/spec/ruact/validation_errors_spec.rb +116 -0
- data/spec/ruact/view_helper_spec.rb +79 -0
- data/spec/spec_helper.rb +0 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +44 -47
- data/vendor/javascript/ruact-server-functions-runtime/index.js +151 -107
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
- data/vendor/javascript/ruact-server-functions-runtime/package.json +2 -2
- data/vendor/javascript/ruact-server-functions-runtime/usequery.test.mjs +187 -0
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +353 -7
- data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +16 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +3 -1
- data/vendor/javascript/vite-plugin-ruact/registry.test.mjs +199 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/bootstrap.jsx +68 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/flight-client.js +235 -0
- data/vendor/javascript/vite-plugin-ruact/runtime/ruact-router.js +473 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +114 -139
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +199 -262
- data/vendor/javascript/vite-plugin-ruact/tsconfig.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json +18 -0
- data/vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json +20 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts +23 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx +90 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx +238 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostList.tsx +339 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostDeleteDialog.tsx +85 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostForm.tsx +216 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/PostList.tsx +269 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/agnostic/ambient.d.ts +78 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/scaffold/ambient.d.ts +166 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/typed-query.test-d.ts +48 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/usequery.test-d.ts +26 -0
- metadata +55 -15
- data/lib/generators/ruact/install/templates/application.jsx.tt +0 -51
- data/lib/ruact/server_action.rb +0 -131
- data/lib/ruact/server_functions/endpoint_controller.rb +0 -237
- data/lib/ruact/server_functions/registry.rb +0 -148
- data/lib/ruact/server_functions/registry_entry.rb +0 -26
- data/lib/ruact/server_functions/standalone_context.rb +0 -103
- data/lib/ruact/server_functions/standalone_dispatcher.rb +0 -178
- data/spec/ruact/server_functions/csrf_request_spec.rb +0 -380
- data/spec/ruact/server_functions/dispatch_request_spec.rb +0 -819
- data/spec/ruact/server_functions/registry_spec.rb +0 -199
- data/spec/ruact/server_functions/standalone_action_spec.rb +0 -224
- data/spec/ruact/server_functions/standalone_context_spec.rb +0 -142
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +0 -273
data/lib/ruact/railtie.rb
CHANGED
|
@@ -8,7 +8,6 @@ module Ruact
|
|
|
8
8
|
require_relative "controller"
|
|
9
9
|
# Story 9.1 — the v2 route-driven marker concern (`include Ruact::Server`).
|
|
10
10
|
require_relative "server"
|
|
11
|
-
require_relative "server_action"
|
|
12
11
|
# Story 9.4 (D8) — requiring ruact/routing installs the `ruact_queries`
|
|
13
12
|
# macro into ActionDispatch::Routing::Mapper (a Mapper extension, NOT a
|
|
14
13
|
# mounted route — the host's routes.rb explicitly mounts each query
|
|
@@ -17,64 +16,8 @@ module Ruact
|
|
|
17
16
|
require_relative "routing"
|
|
18
17
|
end
|
|
19
18
|
|
|
20
|
-
# Story 8.3 — register `app/server_actions/` as a Rails `paths` entry
|
|
21
|
-
# AND as an autoload + eager-load path so Zeitwerk discovers standalone
|
|
22
|
-
# `Ruact::ServerAction` modules the same way it discovers
|
|
23
|
-
# controllers/models/jobs. Registering via `config.paths.add` (vs.
|
|
24
|
-
# `config.autoload_paths <<` only) is what makes
|
|
25
|
-
# `Rails.application.config.paths["app/server_actions"].existent`
|
|
26
|
-
# work — that path enumerator is what
|
|
27
|
-
# {.server_actions_paths_for} consumes during force-load.
|
|
28
|
-
initializer "ruact.register_server_actions_path", before: :set_autoload_paths do |app|
|
|
29
|
-
app.config.paths.add "app/server_actions", with: "app/server_actions"
|
|
30
|
-
candidate = app.root.join("app/server_actions")
|
|
31
|
-
if candidate.directory?
|
|
32
|
-
app.config.autoload_paths << candidate.to_s
|
|
33
|
-
app.config.eager_load_paths << candidate.to_s
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
19
|
rake_tasks { load File.expand_path("../tasks/ruact.rake", __dir__) }
|
|
38
20
|
|
|
39
|
-
# Story 8.1 — clear the action/query registries on every code reload so
|
|
40
|
-
# removed `ruact_action` declarations don't linger in the registry. We hook
|
|
41
|
-
# `before_class_unload` (BEFORE Zeitwerk tears down constants) rather than
|
|
42
|
-
# `to_prepare` (AFTER reload): controller class bodies re-evaluate during
|
|
43
|
-
# the reload itself, and clearing in `to_prepare` would wipe their fresh
|
|
44
|
-
# registrations.
|
|
45
|
-
#
|
|
46
|
-
# First-boot is naturally safe — registries start empty, so there's nothing
|
|
47
|
-
# to clear; the very first controller class-body evaluation populates them.
|
|
48
|
-
# In production this hook never fires (no reloads), which is correct.
|
|
49
|
-
initializer "ruact.attach_registry_clear_hook" do |app|
|
|
50
|
-
app.reloader.before_class_unload do
|
|
51
|
-
Ruact.action_registry.clear!
|
|
52
|
-
Ruact.query_registry.clear!
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Story 8.1 — mount the single gem-managed endpoint that dispatches all
|
|
57
|
-
# `ruact_action` calls. The route is `POST /__ruact/fn/:name`; the
|
|
58
|
-
# controller resolves `:name` against `Ruact.action_registry` (and, once
|
|
59
|
-
# Story 9.1 lands, `Ruact.query_registry`) and delegates execution to the
|
|
60
|
-
# entry's host controller class via Rails' normal `dispatch` flow.
|
|
61
|
-
#
|
|
62
|
-
# Story 8.1 Re-run-2 (2026-05-14) — `routes.prepend` (not `.append`) so
|
|
63
|
-
# the gem's reserved `/__ruact/fn/:name` endpoint wins ahead of any host
|
|
64
|
-
# catch-all (e.g. `match "*path", to: "errors#not_found"` or a wildcard
|
|
65
|
-
# POST handler) that would otherwise swallow every server-function call
|
|
66
|
-
# before Ruact saw it. The `/__ruact/fn/` URL prefix is the gem's
|
|
67
|
-
# documented reserved namespace; hosts that route under `/__ruact/` are
|
|
68
|
-
# using the same prefix at their own risk.
|
|
69
|
-
initializer "ruact.mount_server_functions_route" do |app|
|
|
70
|
-
app.routes.prepend do
|
|
71
|
-
post "/__ruact/fn/:name",
|
|
72
|
-
to: "ruact/server_functions/endpoint#dispatch_action",
|
|
73
|
-
as: :ruact_server_function,
|
|
74
|
-
constraints: { name: /[a-zA-Z_][a-zA-Z0-9_]*/ }
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
21
|
# Load the client manifest at boot (and on each code reload in development).
|
|
79
22
|
# config.to_prepare runs once in production and before every code reload in
|
|
80
23
|
# development, ensuring the manifest is always current without file I/O per
|
|
@@ -104,30 +47,27 @@ module Ruact
|
|
|
104
47
|
ActionView::Base.include(Ruact::ViewHelper)
|
|
105
48
|
ActionView::Template::Handlers::ERB.prepend(Ruact::ErbPreprocessorHook)
|
|
106
49
|
|
|
107
|
-
# Story
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
# once. The endpoint controller would also 404 those names.
|
|
115
|
-
#
|
|
116
|
-
# Story 8.3 — the force-loader was renamed from
|
|
117
|
-
# `force_load_controllers!` to `force_load_server_function_hosts!`
|
|
118
|
-
# and now walks BOTH `app/controllers/**/*_controller.rb` and
|
|
119
|
-
# `app/server_actions/**/*.rb` so standalone modules also register
|
|
120
|
-
# at boot. The old method name is kept as an alias for back-compat
|
|
121
|
-
# with anything outside the gem calling it.
|
|
122
|
-
Ruact::Railtie.force_load_server_function_hosts!
|
|
123
|
-
|
|
50
|
+
# Story 9.9 — the route-driven (v2) codegen is the SOLE writer of the
|
|
51
|
+
# real Rails↔Vite bridge (`tmp/cache/ruact/server-functions.json`, which
|
|
52
|
+
# the Vite plugin renders into `app/javascript/.ruact/server-functions.ts`).
|
|
53
|
+
# The v1 registry path was demolished in this story (epic decision #6:
|
|
54
|
+
# the parallel `.next` target ceases to exist; route-driven codegen owns
|
|
55
|
+
# the real file unconditionally — there are no v1 declarations possible
|
|
56
|
+
# anymore).
|
|
124
57
|
Ruact::Railtie.write_server_functions_snapshot!
|
|
125
|
-
Ruact::ServerFunctions.write_v2_snapshot!(route_set: Rails.application.routes, root: Rails.root)
|
|
126
58
|
end
|
|
127
59
|
|
|
128
60
|
# Detect streaming capability at boot and log the active mode (AC#1–3).
|
|
129
61
|
# Also warns in development if the Vite dev server is not running (AC#4, #7).
|
|
130
62
|
config.after_initialize do
|
|
63
|
+
# Story 9.9 — guaranteed-correct first-boot codegen. By `after_initialize`
|
|
64
|
+
# the route table is fully drawn on EVERY Rails version, so this write
|
|
65
|
+
# always sees the complete route set (the `to_prepare` write above can run
|
|
66
|
+
# mid-finalization on Rails < 8, before routes exist — that write is the
|
|
67
|
+
# dev-reload refresh path; this one is the authoritative boot write).
|
|
68
|
+
# Write-if-changed makes the pair idempotent — at most one real write.
|
|
69
|
+
Ruact::Railtie.write_server_functions_snapshot!
|
|
70
|
+
|
|
131
71
|
Ruact::Railtie.detect_streaming_mode!
|
|
132
72
|
next unless Rails.env.development?
|
|
133
73
|
|
|
@@ -166,138 +106,54 @@ module Ruact
|
|
|
166
106
|
"— run npm run dev for HMR"
|
|
167
107
|
end
|
|
168
108
|
|
|
169
|
-
# Story
|
|
170
|
-
#
|
|
171
|
-
# `
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
# any controller not yet touched.
|
|
177
|
-
#
|
|
178
|
-
# Implementation: glob the `app/controllers` directories listed in the
|
|
179
|
-
# Rails paths configuration and `require_dependency` each
|
|
180
|
-
# `*_controller.rb` file. `require_dependency` works in both Zeitwerk
|
|
181
|
-
# (Rails 7+) and the classic autoloader. On Zeitwerk it is implemented
|
|
182
|
-
# as `Rails.autoloaders.main.load_file(path)` under the hood.
|
|
109
|
+
# Story 9.9 — writes the route-driven (v2) server-functions JSON snapshot
|
|
110
|
+
# to the REAL bridge (`tmp/cache/ruact/server-functions.json`) on every
|
|
111
|
+
# `config.to_prepare`. The Vite plugin renders this JSON into
|
|
112
|
+
# `app/javascript/.ruact/server-functions.ts`. The write is short-circuited
|
|
113
|
+
# when the entries are unchanged (Story 8.0a pitfall #1 — dev mode fires
|
|
114
|
+
# `to_prepare` per request; a naive rewrite would burn IOPS and churn the
|
|
115
|
+
# Vite plugin's chokidar watcher).
|
|
183
116
|
#
|
|
184
|
-
#
|
|
185
|
-
#
|
|
117
|
+
# Cold-boot ordering (folded in from the Story 9.8 host workaround): on the
|
|
118
|
+
# very first boot `to_prepare` can run BEFORE `routes.rb` is drawn, leaving
|
|
119
|
+
# the route set empty so the codegen would emit zero functions. Force the
|
|
120
|
+
# route table to load first via `routes_reloader.execute_unless_loaded` so
|
|
121
|
+
# RouteSource/QuerySource see every route.
|
|
186
122
|
#
|
|
187
|
-
#
|
|
188
|
-
# (
|
|
189
|
-
#
|
|
190
|
-
#
|
|
191
|
-
#
|
|
192
|
-
#
|
|
123
|
+
# The force-load is guarded: when `to_prepare` fires from INSIDE the
|
|
124
|
+
# boot finisher (Rails draws routes during `initialize!`, and on Rails < 8
|
|
125
|
+
# re-entering `execute_unless_loaded` there raises `FrozenError` against the
|
|
126
|
+
# in-progress route set), we SKIP THE WRITE ENTIRELY rather than publish a
|
|
127
|
+
# snapshot from a mid-draw/incomplete route set (which Vite could observe as
|
|
128
|
+
# a transient empty/stale bridge — Codex R1). The authoritative boot write
|
|
129
|
+
# is the `after_initialize` call, which always sees the fully-drawn table;
|
|
130
|
+
# the dev-reload `to_prepare` then refreshes from a settled table.
|
|
193
131
|
#
|
|
194
|
-
# @return [
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
controller_paths_for(Rails.application).each do |dir|
|
|
199
|
-
loaded += force_load_dir(dir, glob: "**/*_controller.rb")
|
|
200
|
-
end
|
|
201
|
-
server_actions_paths_for(Rails.application).each do |dir|
|
|
202
|
-
loaded += force_load_dir(dir, glob: "**/*.rb")
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
if defined?(Rails::Engine)
|
|
206
|
-
Rails::Engine.subclasses.each do |engine_class|
|
|
207
|
-
# Skip the host app itself; `Rails.application.class` is a
|
|
208
|
-
# `Rails::Engine` subclass and was already covered above.
|
|
209
|
-
next if engine_class == Rails.application.class
|
|
210
|
-
|
|
211
|
-
engine = safe_engine_instance(engine_class)
|
|
212
|
-
next unless engine
|
|
213
|
-
|
|
214
|
-
controller_paths_for(engine).each { |dir| loaded += force_load_dir(dir, glob: "**/*_controller.rb") }
|
|
215
|
-
server_actions_paths_for(engine).each { |dir| loaded += force_load_dir(dir, glob: "**/*.rb") }
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
loaded
|
|
220
|
-
rescue LoadError, NameError => e
|
|
221
|
-
raise Ruact::Error,
|
|
222
|
-
"ruact: failed to force-load a server-function host while populating " \
|
|
223
|
-
"Ruact.action_registry: #{e.class}: #{e.message}. The gem " \
|
|
224
|
-
"force-loads `app/controllers/**/*_controller.rb` and " \
|
|
225
|
-
"`app/server_actions/**/*.rb` at `config.to_prepare` so " \
|
|
226
|
-
"registries are complete on first boot."
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
# Story 8.3 — back-compat alias. Older code paths (rake tasks shipped
|
|
230
|
-
# in previous gem versions, downstream tooling) may call the old
|
|
231
|
-
# name; the new name describes the wider behavior accurately.
|
|
232
|
-
class << self
|
|
233
|
-
alias force_load_controllers! force_load_server_function_hosts!
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# @param engine [Rails::Engine] either `Rails.application` or a mounted engine
|
|
237
|
-
# @return [Array<String>] existing controller directory paths
|
|
238
|
-
def self.controller_paths_for(engine)
|
|
239
|
-
return [] unless engine.respond_to?(:config) && engine.config.respond_to?(:paths)
|
|
240
|
-
|
|
241
|
-
paths = engine.config.paths["app/controllers"]
|
|
242
|
-
return [] unless paths.respond_to?(:existent)
|
|
243
|
-
|
|
244
|
-
paths.existent
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# @param dir [String]
|
|
248
|
-
# @param glob [String] glob pattern relative to +dir+; defaults to
|
|
249
|
-
# `**/*_controller.rb` for back-compat with pre-Story-8.3 callers.
|
|
250
|
-
# @return [Integer] number of files loaded
|
|
251
|
-
def self.force_load_dir(dir, glob: "**/*_controller.rb")
|
|
252
|
-
Dir.glob("#{dir}/#{glob}").each do |file|
|
|
253
|
-
require_dependency(file)
|
|
254
|
-
end.length
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Story 8.3 — locates `app/server_actions/` for the host application
|
|
258
|
-
# or a mounted engine. Uses the Rails `paths` enumerator (populated by
|
|
259
|
-
# the Railtie initializer) when present, falling back to a direct
|
|
260
|
-
# `engine.root.join` lookup for engines that haven't registered the
|
|
261
|
-
# path. Returns an empty array when the directory doesn't exist —
|
|
262
|
-
# silent no-op for hosts that don't use standalone actions.
|
|
263
|
-
def self.server_actions_paths_for(engine)
|
|
264
|
-
if engine.respond_to?(:config) && engine.config.respond_to?(:paths)
|
|
265
|
-
paths = engine.config.paths["app/server_actions"]
|
|
266
|
-
if paths.respond_to?(:existent)
|
|
267
|
-
existent = paths.existent
|
|
268
|
-
return existent unless existent.empty?
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
return [] unless engine.respond_to?(:root) && engine.root
|
|
273
|
-
|
|
274
|
-
candidate = engine.root.join("app/server_actions")
|
|
275
|
-
candidate.directory? ? [candidate.to_s] : []
|
|
276
|
-
end
|
|
132
|
+
# @return [Array<Hash>, nil] the exposed v2 entries, or nil if the write was
|
|
133
|
+
# skipped because routes were mid-draw.
|
|
134
|
+
def self.write_server_functions_snapshot!
|
|
135
|
+
return nil unless force_routes_loaded!
|
|
277
136
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
# `.instance` blows up will surface on first request anyway.
|
|
282
|
-
def self.safe_engine_instance(engine_class)
|
|
283
|
-
engine_class.instance
|
|
284
|
-
rescue StandardError
|
|
285
|
-
nil
|
|
137
|
+
Ruact::ServerFunctions.write_v2_snapshot!(
|
|
138
|
+
route_set: Rails.application.routes, root: Rails.root
|
|
139
|
+
)
|
|
286
140
|
end
|
|
287
141
|
|
|
288
|
-
#
|
|
289
|
-
#
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
#
|
|
142
|
+
# Idempotently force the route table to load. Returns true when the table is
|
|
143
|
+
# safely loaded (so the caller may read it), false when the force re-entered
|
|
144
|
+
# the boot finisher's in-progress draw (Rails < 8 raises `FrozenError`
|
|
145
|
+
# there) — in which case the caller must NOT write, leaving the table to the
|
|
146
|
+
# routes initializer + the later `after_initialize` write.
|
|
293
147
|
#
|
|
294
|
-
# @return [Boolean] true if
|
|
295
|
-
def self.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
148
|
+
# @return [Boolean] true if routes are safe to read, false if mid-draw.
|
|
149
|
+
def self.force_routes_loaded!
|
|
150
|
+
reloader = Rails.application.routes_reloader
|
|
151
|
+
return true unless reloader.respond_to?(:execute_unless_loaded)
|
|
152
|
+
|
|
153
|
+
reloader.execute_unless_loaded
|
|
154
|
+
true
|
|
155
|
+
rescue FrozenError
|
|
156
|
+
false
|
|
301
157
|
end
|
|
302
158
|
|
|
303
159
|
# Checks whether the manifest exists and either warns (dev) or raises (prod).
|
data/lib/ruact/server.rb
CHANGED
|
@@ -14,6 +14,7 @@ require_relative "../ruact"
|
|
|
14
14
|
require_relative "server_functions/error_rendering"
|
|
15
15
|
require_relative "server_functions/bucket_two_payload"
|
|
16
16
|
require_relative "server_functions/name_bridge"
|
|
17
|
+
require_relative "validation_errors_collector"
|
|
17
18
|
|
|
18
19
|
module Ruact
|
|
19
20
|
# Story 9.1 (route-driven redesign, Phase A) — the v2 server-functions
|
|
@@ -59,16 +60,16 @@ module Ruact
|
|
|
59
60
|
# Contract simplification: the concern assumes the host includes it after
|
|
60
61
|
# `protect_from_forgery`; no runtime callback-order verifier runs here.
|
|
61
62
|
#
|
|
62
|
-
# Both bodies live in {Ruact::ServerFunctions::ErrorRendering}
|
|
63
|
-
# the v1
|
|
64
|
-
#
|
|
65
|
-
# construction. Dual-bucket response negotiation (ivar serialization,
|
|
63
|
+
# Both bodies live in {Ruact::ServerFunctions::ErrorRendering} (Story 9.9 —
|
|
64
|
+
# this concern is now the sole home; the v1 endpoint that previously shared it
|
|
65
|
+
# was demolished). Dual-bucket response negotiation (ivar serialization,
|
|
66
66
|
# `$redirect`, 204, `Vary: Accept`) is Story 9.2; this concern only
|
|
67
67
|
# contributes the discrimination predicate 9.2 will reuse.
|
|
68
68
|
module Server
|
|
69
69
|
extend ActiveSupport::Concern
|
|
70
70
|
|
|
71
71
|
include Ruact::ServerFunctions::ErrorRendering
|
|
72
|
+
include Ruact::ValidationErrorsCollector
|
|
72
73
|
|
|
73
74
|
included do
|
|
74
75
|
# Story 8.5 salvage — prepended so the size check wins the race against
|
|
@@ -159,7 +160,7 @@ module Ruact
|
|
|
159
160
|
raise Ruact::ConfigurationError,
|
|
160
161
|
"ruact_function_name :#{action}, as: #{as.inspect} — " \
|
|
161
162
|
"\"#{js}\" is a reserved JS word or is already bound by the ruact runtime " \
|
|
162
|
-
"(`
|
|
163
|
+
"(`_makeServerFunction` / `_makeQuery` / `revalidate` / `useQuery`); pick another name"
|
|
163
164
|
end
|
|
164
165
|
|
|
165
166
|
__ruact_function_name_overrides[action.to_s] = js
|
|
@@ -194,7 +195,28 @@ module Ruact
|
|
|
194
195
|
def default_render(*)
|
|
195
196
|
return super unless __ruact_function_call?
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
# Story 13.3 (FR98) — the collector's framework-internal ivars (and a stray
|
|
199
|
+
# dev `@errors`) are dropped from the serialized assigns; Rails' own
|
|
200
|
+
# `view_assigns` only filters a fixed set of `@_` ivars, not every `@__`
|
|
201
|
+
# one (see {Ruact::ValidationErrorsCollector::RESERVED_ASSIGN_KEYS}).
|
|
202
|
+
assigns = view_assigns.except(*ValidationErrorsCollector::RESERVED_ASSIGN_KEYS)
|
|
203
|
+
|
|
204
|
+
# Story 13.3 (FR98, AC3) — when the host opted into the validation-error
|
|
205
|
+
# round-trip (`ruact_errors(@record)` was called this request), inject the
|
|
206
|
+
# collected errors under the reserved JSON key `"errors"` alongside the
|
|
207
|
+
# serialized ivars, even on an otherwise-empty assigns set: an opted-in
|
|
208
|
+
# success must still surface `{"errors": {}}` so the client's `await`
|
|
209
|
+
# result carries `result.errors` on a single code path.
|
|
210
|
+
if __ruact_errors_touched?
|
|
211
|
+
payload = ServerFunctions::BucketTwoPayload.build(
|
|
212
|
+
assigns, strict: Ruact.config.strict_serialization
|
|
213
|
+
)
|
|
214
|
+
payload["errors"] = __ruact_errors
|
|
215
|
+
return render(json: payload)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# An UNTOUCHED collector changes nothing — the 9.2 `204 No Content`
|
|
219
|
+
# empty-Bucket-2 contract is preserved.
|
|
198
220
|
return head(:no_content) if assigns.empty?
|
|
199
221
|
|
|
200
222
|
render json: ServerFunctions::BucketTwoPayload.build(
|