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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Story 13.3 (FR98) — unit specs for the pure validation-error normalizer
|
|
4
|
+
# Ruact::ServerFunctions::ValidationErrors.normalize. The normalizer turns an
|
|
5
|
+
# ActiveModel-ish record, a raw ActiveModel::Errors, or a pre-shaped Hash into
|
|
6
|
+
# the canonical wire shape `{ [attribute: String] => Array<String> }` (full
|
|
7
|
+
# messages, `base` key, `{}` on a valid/empty/nil source, idempotent on
|
|
8
|
+
# already-canonical input). Pure — no Rails/request/Ruact.config reads.
|
|
9
|
+
|
|
10
|
+
require "spec_helper"
|
|
11
|
+
require "active_model"
|
|
12
|
+
|
|
13
|
+
require "ruact/server_functions/validation_errors"
|
|
14
|
+
|
|
15
|
+
module Ruact
|
|
16
|
+
module ServerFunctions
|
|
17
|
+
RSpec.describe ValidationErrors, :story_13_3 do
|
|
18
|
+
# A minimal ActiveModel record so the normalizer can exercise the real
|
|
19
|
+
# `errors.group_by_attribute` / `ActiveModel::Error#full_message` API
|
|
20
|
+
# (present since Rails 6.1; gem pins `rails ~> 8.0`).
|
|
21
|
+
let(:record_class) do
|
|
22
|
+
Class.new do
|
|
23
|
+
include ActiveModel::Model
|
|
24
|
+
include ActiveModel::Attributes
|
|
25
|
+
|
|
26
|
+
attribute :title, :string
|
|
27
|
+
attribute :body, :string
|
|
28
|
+
|
|
29
|
+
def self.name = "ValidationErrorsSpecPost"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe ".normalize" do
|
|
34
|
+
context "with a record responding to #errors" do
|
|
35
|
+
it "returns the canonical { attr => [full_message] } shape" do
|
|
36
|
+
record = record_class.new
|
|
37
|
+
record.errors.add(:title, :blank) # "Title can't be blank"
|
|
38
|
+
|
|
39
|
+
expect(described_class.normalize(record)).to eq("title" => ["Title can't be blank"])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "keys base-level errors under the string 'base' with the bare message" do
|
|
43
|
+
record = record_class.new
|
|
44
|
+
record.errors.add(:base, "is invalid overall")
|
|
45
|
+
|
|
46
|
+
expect(described_class.normalize(record)).to eq("base" => ["is invalid overall"])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "groups multiple attributes and multiple messages per attribute" do
|
|
50
|
+
record = record_class.new
|
|
51
|
+
record.errors.add(:title, :blank) # "Title can't be blank"
|
|
52
|
+
record.errors.add(:title, "is too short") # "Title is too short"
|
|
53
|
+
record.errors.add(:body, "is invalid") # "Body is invalid"
|
|
54
|
+
|
|
55
|
+
expect(described_class.normalize(record)).to eq(
|
|
56
|
+
"title" => ["Title can't be blank", "Title is too short"],
|
|
57
|
+
"body" => ["Body is invalid"]
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "returns {} for a record with no errors (success symmetry)" do
|
|
62
|
+
expect(described_class.normalize(record_class.new)).to eq({})
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context "with a raw ActiveModel::Errors" do
|
|
67
|
+
it "normalizes the Errors object directly" do
|
|
68
|
+
record = record_class.new
|
|
69
|
+
record.errors.add(:title, :blank)
|
|
70
|
+
|
|
71
|
+
expect(described_class.normalize(record.errors)).to eq("title" => ["Title can't be blank"])
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context "with a pre-shaped Hash" do
|
|
76
|
+
it "stringifies keys and leaves canonical array values untouched (idempotent)" do
|
|
77
|
+
canonical = { "title" => ["Title can't be blank"] }
|
|
78
|
+
|
|
79
|
+
expect(described_class.normalize(canonical)).to eq(canonical)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "stringifies symbol keys" do
|
|
83
|
+
expect(described_class.normalize(title: ["x"])).to eq("title" => ["x"])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "coerces a scalar value into a one-element array" do
|
|
87
|
+
expect(described_class.normalize("title" => "can't be blank")).to eq("title" => ["can't be blank"])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "coerces non-string message values to strings" do
|
|
91
|
+
expect(described_class.normalize("count" => [1, 2])).to eq("count" => %w[1 2])
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "returns {} for an empty Hash" do
|
|
95
|
+
expect(described_class.normalize({})).to eq({})
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
context "with nil / empty sources" do
|
|
100
|
+
it "returns {} for nil" do
|
|
101
|
+
expect(described_class.normalize(nil)).to eq({})
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "is idempotent — normalizing its own output yields the same Hash" do
|
|
106
|
+
record = record_class.new
|
|
107
|
+
record.errors.add(:title, :blank)
|
|
108
|
+
record.errors.add(:base, "is invalid overall")
|
|
109
|
+
|
|
110
|
+
once = described_class.normalize(record)
|
|
111
|
+
expect(described_class.normalize(once)).to eq(once)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "spec_helper"
|
|
4
4
|
require "active_support/core_ext/string/output_safety"
|
|
5
|
+
require "active_support/string_inquirer"
|
|
5
6
|
|
|
6
7
|
module Ruact
|
|
7
8
|
RSpec.describe ViewHelper do
|
|
@@ -48,5 +49,83 @@ module Ruact
|
|
|
48
49
|
.to raise_error(Ruact::Error, /__ruact_component__ called outside a ruact_render flow/)
|
|
49
50
|
end
|
|
50
51
|
end
|
|
52
|
+
|
|
53
|
+
# Story 14.2 (FR104) — the public JS-asset helper. Emits the dev/prod
|
|
54
|
+
# bootstrap entry `<script>` tags (re-targeting `virtual:ruact/bootstrap`)
|
|
55
|
+
# plus the `__FLIGHT_DATA` inline script. The controller delegates to this
|
|
56
|
+
# one implementation (parity asserted in controller_spec).
|
|
57
|
+
describe "#ruact_js_assets", :story_14_2 do
|
|
58
|
+
let(:asset_helper) do
|
|
59
|
+
obj = Object.new
|
|
60
|
+
obj.extend(described_class)
|
|
61
|
+
obj
|
|
62
|
+
end
|
|
63
|
+
let(:payload) { "0:[\"$\",\"div\",null,{}]\n" }
|
|
64
|
+
|
|
65
|
+
context "when in dev with the Vite dev server running" do
|
|
66
|
+
before do
|
|
67
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("development"))
|
|
68
|
+
allow(asset_helper).to receive(:vite_dev_running?).and_return(true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "emits the react-refresh preamble, @vite/client, and the bootstrap module", :aggregate_failures do
|
|
72
|
+
html = asset_helper.ruact_js_assets(payload)
|
|
73
|
+
expect(html).to include("__vite_plugin_react_preamble_installed__")
|
|
74
|
+
expect(html).to include("http://localhost:5173/@vite/client")
|
|
75
|
+
# Targets the virtual entry at the dev server's /@id/__x00__ URL — NOT
|
|
76
|
+
# a stale application.jsx path.
|
|
77
|
+
expect(html).to include("/@id/__x00__#{Ruact.bootstrap_virtual_id}")
|
|
78
|
+
expect(html).not_to include("application.jsx")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "includes the __FLIGHT_DATA inline bootstrap script", :aggregate_failures do
|
|
82
|
+
html = asset_helper.ruact_js_assets(payload)
|
|
83
|
+
expect(html).to include("__FLIGHT_DATA")
|
|
84
|
+
expect(html).to include("d.push(")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "returns an html_safe buffer" do
|
|
88
|
+
expect(asset_helper.ruact_js_assets(payload)).to be_html_safe
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context "when in prod (Vite manifest lookup)" do
|
|
93
|
+
before do
|
|
94
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "looks up the manifest with the SAME id the generated vite.config input uses (AC3 — no drift)" do
|
|
98
|
+
allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
|
|
99
|
+
asset_helper.ruact_js_assets(payload)
|
|
100
|
+
expect(asset_helper).to have_received(:vite_manifest_entry).with(Ruact.bootstrap_virtual_id)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "emits the hashed bootstrap URL from the manifest entry", :aggregate_failures do
|
|
104
|
+
allow(asset_helper).to receive(:vite_manifest_entry)
|
|
105
|
+
.with(Ruact.bootstrap_virtual_id).and_return({ "file" => "bootstrap-abc123.js" })
|
|
106
|
+
html = asset_helper.ruact_js_assets(payload)
|
|
107
|
+
expect(html).to include(%(src="/assets/bootstrap-abc123.js"))
|
|
108
|
+
expect(html).to include("__FLIGHT_DATA")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "falls back to /assets/application.js when the manifest entry is missing" do
|
|
112
|
+
allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
|
|
113
|
+
expect(asset_helper.ruact_js_assets(payload)).to include("/assets/application.js")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
context "without a Flight payload (entry tags only)" do
|
|
118
|
+
before do
|
|
119
|
+
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new("production"))
|
|
120
|
+
allow(asset_helper).to receive(:vite_manifest_entry).and_return(nil)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "emits only the entry tag (no __FLIGHT_DATA)", :aggregate_failures do
|
|
124
|
+
html = asset_helper.ruact_js_assets
|
|
125
|
+
expect(html).not_to include("__FLIGHT_DATA")
|
|
126
|
+
expect(html).to include("<script type=\"module\"")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
51
130
|
end
|
|
52
131
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -50,11 +50,6 @@ RSpec.configure do |config|
|
|
|
50
50
|
config.before do
|
|
51
51
|
Ruact.instance_variable_set(:@config, nil)
|
|
52
52
|
Ruact.instance_variable_set(:@configured_at_least_once, false)
|
|
53
|
-
# Story 8.0a: both server-function registries are lazy-initialized module
|
|
54
|
-
# singletons. Wipe them between examples so register/clear specs in one file
|
|
55
|
-
# cannot bleed entries into another under random order.
|
|
56
|
-
Ruact.instance_variable_set(:@action_registry, nil)
|
|
57
|
-
Ruact.instance_variable_set(:@query_registry, nil)
|
|
58
53
|
# Story 9.3: specs that set `Rails.logger = instance_double(Logger)` (e.g.
|
|
59
54
|
# railtie_spec, configuration_spec) leave a per-example double on the global
|
|
60
55
|
# after they finish; rspec then refuses to call it from the NEXT example.
|
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// TypeScript declarations for the route-driven server-functions runtime.
|
|
2
2
|
// Mirrors the JS exports in `index.js` so the generated module's
|
|
3
|
-
// `import {
|
|
4
|
-
// `tsc --noEmit
|
|
3
|
+
// `import { _makeServerFunction } from "ruact/server-functions-runtime"`
|
|
4
|
+
// resolves under `tsc --noEmit`. Story 9.9 demolished the v1 `_makeRef` accessor.
|
|
5
5
|
//
|
|
6
|
-
// The
|
|
7
|
-
// `
|
|
8
|
-
//
|
|
9
|
-
// (Story 8.2 owns the codegen signature widening if it picks the FormData
|
|
10
|
-
// path). Devs writing call sites against the 8.0a-emitted module continue
|
|
11
|
-
// to see the conservative Record<string, unknown> signature; the FormData
|
|
12
|
-
// branch only fires through Story 8.2's `<form action={fn}>` wiring.
|
|
6
|
+
// The runtime accepts a wider `FormData` argument as well as the conservative
|
|
7
|
+
// `Record<string, unknown>` shape; the FormData branch fires through the
|
|
8
|
+
// `<form action={fn}>` wiring.
|
|
13
9
|
|
|
14
10
|
/**
|
|
15
11
|
* Re-run-3 (2026-05-15), refined Re-run-4 (2026-05-15) — local alias
|
|
@@ -36,46 +32,25 @@ type RuactResponse = typeof globalThis extends { Response: new (...args: never[]
|
|
|
36
32
|
: unknown;
|
|
37
33
|
|
|
38
34
|
/**
|
|
39
|
-
*
|
|
40
|
-
* given Ruby symbol name. The accessor, when invoked, POSTs the args to
|
|
41
|
-
* `/__ruact/fn/${name}`.
|
|
42
|
-
*
|
|
43
|
-
* Story 8.2 (refined 2026-05-17 per review patch R1) — the return type
|
|
44
|
-
* is an intersection of FOUR call signatures so the same exported
|
|
45
|
-
* reference is usable from every call site:
|
|
46
|
-
*
|
|
47
|
-
* 1. `()` / `(args)` / `(prevState, formData)` — direct callers and
|
|
48
|
-
* `useActionState`'s two-arg invocation; returns `Promise<unknown>`.
|
|
49
|
-
* 2. `(formData: FormData)` — assignable to React 19's `<form action>`
|
|
50
|
-
* prop, which is typed as `(formData: FormData) => void | Promise<void>`.
|
|
51
|
-
* Promise generics are invariant in TS, so `Promise<unknown>` is
|
|
52
|
-
* NOT assignable to `Promise<void>` even via the void-discard rule;
|
|
53
|
-
* the intersection lets `<form action={createPost}>` typecheck
|
|
54
|
-
* DIRECTLY against the emitted module without a call-site cast.
|
|
55
|
-
*
|
|
56
|
-
* Runtime behavior is unchanged — `_makeRef` always resolves with the
|
|
57
|
-
* JSON-decoded value. The `Promise<void>` overload is a TYPE-ONLY
|
|
58
|
-
* surface: when React invokes the function from a `<form action>` prop,
|
|
59
|
-
* the return value is discarded by React anyway.
|
|
60
|
-
*/
|
|
61
|
-
export function _makeRef(
|
|
62
|
-
name: string,
|
|
63
|
-
): ((
|
|
64
|
-
arg1?: Record<string, unknown> | RuactFormData,
|
|
65
|
-
arg2?: RuactFormData | Record<string, unknown>,
|
|
66
|
-
) => Promise<unknown>) &
|
|
67
|
-
((formData: RuactFormData) => Promise<void>);
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Story 9.3 — the route-driven (v2) accessor. The codegen emits
|
|
35
|
+
* Story 9.3 — the route-driven accessor. The codegen emits
|
|
71
36
|
* `_makeServerFunction({ method, path, segments })` for every non-GET routed
|
|
72
37
|
* action on a `Ruact::Server` controller. The returned callable targets the
|
|
73
38
|
* REAL Rails route + verb (e.g. `POST /posts`, `PUT /posts/:id`), interpolating
|
|
74
39
|
* dynamic path segments by name from the single call argument, and follows a
|
|
75
40
|
* Bucket-2 `{ "$redirect": "<path>" }` response client-side.
|
|
76
41
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
42
|
+
* Story 8.2 (refined 2026-05-17 per review patch R1) — the return type is an
|
|
43
|
+
* intersection of call signatures so the same exported reference is usable from
|
|
44
|
+
* every call site:
|
|
45
|
+
*
|
|
46
|
+
* 1. `()` / `(args)` / `(prevState, formData)` — direct callers and
|
|
47
|
+
* `useActionState`'s two-arg invocation; returns `Promise<unknown>`.
|
|
48
|
+
* 2. `(formData: FormData)` — assignable to React 19's `<form action>`
|
|
49
|
+
* prop, which is typed as `(formData: FormData) => void | Promise<void>`.
|
|
50
|
+
* Promise generics are invariant in TS, so `Promise<unknown>` is NOT
|
|
51
|
+
* assignable to `Promise<void>` even via the void-discard rule; the
|
|
52
|
+
* intersection lets `<form action={createPost}>` typecheck DIRECTLY
|
|
53
|
+
* against the emitted module without a call-site cast.
|
|
79
54
|
*/
|
|
80
55
|
export function _makeServerFunction(descriptor: {
|
|
81
56
|
method: string;
|
|
@@ -113,11 +88,33 @@ export function _makeQuery(descriptor: {
|
|
|
113
88
|
* resolution; `error` carries the structured {@link RuactActionError} on
|
|
114
89
|
* failure. A superseded in-flight response is dropped.
|
|
115
90
|
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
91
|
+
* Story 9.6 — identical concurrent calls (same reference + same params,
|
|
92
|
+
* order-independent) share ONE in-flight request. Dedup is in-flight only:
|
|
93
|
+
* once a request settles the shared entry is dropped, so a fresh mount
|
|
94
|
+
* refetches (no TTL cache, no stale-while-revalidate).
|
|
95
|
+
*
|
|
96
|
+
* Story 13.4 — the `reference` parameter accepts ANY codegen-emitted accessor
|
|
97
|
+
* shape: the pre-13.4 declaration typed it `(params?: Record<string, unknown>)
|
|
98
|
+
* => Promise<unknown>`, which a now-TYPED query accessor (e.g. `(params: { term:
|
|
99
|
+
* string | number | boolean | null }) => Promise<unknown>`) is NOT assignable to
|
|
100
|
+
* under `strict` (contravariant param mismatch). Widening to `(...args: never[])
|
|
101
|
+
* => Promise<unknown>` accepts every typed/untyped/no-kwargs accessor without a
|
|
102
|
+
* cast and without a regression. `T` (the return type) stays the explicitly-
|
|
103
|
+
* specifiable return-typing generic — `useQuery<User[]>(searchUsers, { term })`.
|
|
104
|
+
*
|
|
105
|
+
* SCOPE BOUNDARY (intentional): the second `params` argument stays
|
|
106
|
+
* `Record<string, unknown>` — `useQuery` does NOT re-check the params SHAPE
|
|
107
|
+
* against the accessor. Per-param compile-time checking (missing-required,
|
|
108
|
+
* unknown-key, wrong-typed) is delivered at the ACCESSOR's own call site
|
|
109
|
+
* (`searchUsers({ term })`), which is where FR99 narrows the type. Inferring the
|
|
110
|
+
* params type here would require a second generic `P` that is impossible to make
|
|
111
|
+
* both inferred-from-the-accessor AND compatible with the explicit
|
|
112
|
+
* `useQuery<User[]>(…)` return-typing form (TS short-circuits inference of a
|
|
113
|
+
* defaulted trailing type parameter, and rejects a partial type-argument list
|
|
114
|
+
* for a non-defaulted one). This matches the pre-13.4 `params` contract exactly.
|
|
118
115
|
*/
|
|
119
116
|
export function useQuery<T = unknown>(
|
|
120
|
-
reference: (
|
|
117
|
+
reference: (...args: never[]) => Promise<unknown>,
|
|
121
118
|
params?: Record<string, unknown>,
|
|
122
119
|
): { data: T | undefined; loading: boolean; error: unknown };
|
|
123
120
|
|