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
|
@@ -19,6 +19,24 @@ module Ruact
|
|
|
19
19
|
# "name": "default"
|
|
20
20
|
# }
|
|
21
21
|
# }
|
|
22
|
+
#
|
|
23
|
+
# Story 13.5 (FR100) — an entry MAY carry an optional, purely additive
|
|
24
|
+
# +contract+ field when the component opts in by exporting +__ruactContract+
|
|
25
|
+
# from its +.tsx+ (the Vite plugin extracts it names-only). Shape:
|
|
26
|
+
#
|
|
27
|
+
# "LikeButton": {
|
|
28
|
+
# "id": ..., "name": ..., "chunks": [...],
|
|
29
|
+
# "contract": {
|
|
30
|
+
# "props": { "postId" => "required", "initialCount" => "optional" },
|
|
31
|
+
# "slots": { "header" => "optional" }, # optional
|
|
32
|
+
# "passthrough": false # optional
|
|
33
|
+
# }
|
|
34
|
+
# }
|
|
35
|
+
#
|
|
36
|
+
# A component without the export has NO +contract+ key (back-compatible: every
|
|
37
|
+
# existing manifest + reader is unaffected). {#contract_for} reads it for the
|
|
38
|
+
# Story 13.5 preprocess-time call-site validator; +nil+ means "no contract →
|
|
39
|
+
# no validation" (fail open).
|
|
22
40
|
class ClientManifest
|
|
23
41
|
# Used by Flight::Serializer to produce I rows.
|
|
24
42
|
# Returns the metadata array the client expects: [id, name, chunks]
|
|
@@ -62,6 +80,24 @@ module Ruact
|
|
|
62
80
|
end
|
|
63
81
|
end
|
|
64
82
|
|
|
83
|
+
# Story 13.5 (FR100) — return the optional component +contract+ Hash for
|
|
84
|
+
# +name+, or +nil+ when the component declared none (or is absent from the
|
|
85
|
+
# manifest). Honors the same co-located/shared +resolve_key+ precedence as
|
|
86
|
+
# {#reference_for}, so a co-located component's contract is found when the
|
|
87
|
+
# call site's +controller_path+ is known. A pure read (no memoization, no
|
|
88
|
+
# mutation) — safe on the frozen manifest, never raises for an unknown name
|
|
89
|
+
# (the validator fails open). Consumed by {Ruact::ComponentContract} from
|
|
90
|
+
# the ERB preprocessor.
|
|
91
|
+
#
|
|
92
|
+
# @param name [String] PascalCase component name (e.g. "LikeButton")
|
|
93
|
+
# @param controller_path [String, nil] e.g. "posts" — biases toward a
|
|
94
|
+
# co-located key ("posts/_like_button") when present.
|
|
95
|
+
# @return [Hash, nil] the contract Hash, or nil when none is declared.
|
|
96
|
+
def contract_for(name, controller_path: nil)
|
|
97
|
+
entry = entries_by_name[resolve_key(name, controller_path)]
|
|
98
|
+
entry && entry["contract"]
|
|
99
|
+
end
|
|
100
|
+
|
|
65
101
|
# Load from a file path (JSON).
|
|
66
102
|
# Pre-warms the reference cache and freezes the manifest so it cannot be
|
|
67
103
|
# mutated at runtime (AC#5). Pre-warming is required because Ruby's freeze
|
|
@@ -131,7 +167,7 @@ module Ruact
|
|
|
131
167
|
|
|
132
168
|
pool.each do |key|
|
|
133
169
|
comparable = comparable_name_for(key).downcase
|
|
134
|
-
distance =
|
|
170
|
+
distance = StringDistance.damerau_levenshtein(target, comparable)
|
|
135
171
|
next if distance > 2
|
|
136
172
|
|
|
137
173
|
in_scope = controller_path && key.start_with?("#{controller_path}/")
|
|
@@ -155,41 +191,6 @@ module Ruact
|
|
|
155
191
|
basename.split("_").map(&:capitalize).join
|
|
156
192
|
end
|
|
157
193
|
|
|
158
|
-
# Damerau-Levenshtein distance — like classic Levenshtein but treats
|
|
159
|
-
# an adjacent transposition (e.g. "ke"↔"ek") as a single edit. Component
|
|
160
|
-
# names are short (≤ 30 chars in practice) so the full O(m·n) DP table
|
|
161
|
-
# is fine; the readability win over the two-row Levenshtein trick is
|
|
162
|
-
# worth the extra ~30 cells of allocation in the failure path.
|
|
163
|
-
# rubocop:disable Metrics/AbcSize
|
|
164
|
-
def self.damerau_levenshtein_distance(left, right)
|
|
165
|
-
return right.length if left.empty?
|
|
166
|
-
return left.length if right.empty?
|
|
167
|
-
|
|
168
|
-
m = left.length
|
|
169
|
-
n = right.length
|
|
170
|
-
d = Array.new(m + 1) { Array.new(n + 1, 0) }
|
|
171
|
-
(0..m).each { |i| d[i][0] = i }
|
|
172
|
-
(0..n).each { |j| d[0][j] = j }
|
|
173
|
-
|
|
174
|
-
(1..m).each do |i|
|
|
175
|
-
(1..n).each do |j|
|
|
176
|
-
cost = left[i - 1] == right[j - 1] ? 0 : 1
|
|
177
|
-
d[i][j] = [
|
|
178
|
-
d[i - 1][j] + 1, # deletion
|
|
179
|
-
d[i][j - 1] + 1, # insertion
|
|
180
|
-
d[i - 1][j - 1] + cost # substitution
|
|
181
|
-
].min
|
|
182
|
-
if i > 1 && j > 1 && left[i - 1] == right[j - 2] && left[i - 2] == right[j - 1]
|
|
183
|
-
d[i][j] = [d[i][j], d[i - 2][j - 2] + cost].min
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
d[m][n]
|
|
189
|
-
end
|
|
190
|
-
# rubocop:enable Metrics/AbcSize
|
|
191
|
-
private_class_method :damerau_levenshtein_distance
|
|
192
|
-
|
|
193
194
|
# Returns the manifest key to use for +name+ given an optional +controller_path+.
|
|
194
195
|
# Co-located key format: "<controller_path>/_<underscored_name>" (e.g. "posts/_like_button").
|
|
195
196
|
# Co-located takes precedence when both keys exist.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruact
|
|
4
|
+
# Story 13.5 (FR100) — compile-time component contract (HEEx-style `attr`/
|
|
5
|
+
# `slot`). Validates a `<Component .../>` ERB call site against the
|
|
6
|
+
# component's OPT-IN contract at preprocess time, so a missing required prop,
|
|
7
|
+
# a typo'd prop name, or a missing required slot surfaces an error at the call
|
|
8
|
+
# site (file:line) BEFORE the page renders — not as a silent `undefined` in
|
|
9
|
+
# the browser.
|
|
10
|
+
#
|
|
11
|
+
# This is the CONSUMER-side mirror of `ruact_props` (which validates the
|
|
12
|
+
# PRODUCER side at class-load, +serializable.rb+). It is NAME-level only:
|
|
13
|
+
# prop VALUES are arbitrary render-time Ruby expressions the preprocessor
|
|
14
|
+
# never evaluates, so only names + presence + slots are checkable here (value
|
|
15
|
+
# typing for the server boundary already landed in Story 13.4).
|
|
16
|
+
#
|
|
17
|
+
# The contract Hash is the one the Vite plugin extracted into the manifest:
|
|
18
|
+
#
|
|
19
|
+
# {
|
|
20
|
+
# "props" => { "postId" => "required", "initialCount" => "optional" },
|
|
21
|
+
# "slots" => { "header" => "optional" }, # optional
|
|
22
|
+
# "passthrough" => false # optional
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# Pure + stateless (explicit class methods) so it is trivially testable in
|
|
26
|
+
# isolation and carries no global state.
|
|
27
|
+
module ComponentContract
|
|
28
|
+
# Validate a call site's prop NAMES against +contract+. Raises
|
|
29
|
+
# {Ruact::ComponentContractError} (a {Ruact::PreprocessorError}) on the
|
|
30
|
+
# first violation. A +nil+ contract is a no-op (fail open / opt-in).
|
|
31
|
+
#
|
|
32
|
+
# @param component_name [String] e.g. "LikeButton"
|
|
33
|
+
# @param prop_names [Array<String>] names parsed from the call site (e.g.
|
|
34
|
+
# `["postId", "initialCount"]` for a `<LikeButton>` tag with those props)
|
|
35
|
+
# @param contract [Hash, nil] the manifest contract Hash, or nil
|
|
36
|
+
# @param at [Hash] call-site location for the message with optional keys
|
|
37
|
+
# +file+ (from `template.identifier`), +line+ (the 1-based call-site
|
|
38
|
+
# line), and +snippet+ (the offending tag text)
|
|
39
|
+
# @return [void]
|
|
40
|
+
# @raise [Ruact::ComponentContractError] on a contract violation
|
|
41
|
+
def self.validate(component_name:, prop_names:, contract:, at: {})
|
|
42
|
+
return if contract.nil?
|
|
43
|
+
|
|
44
|
+
props = normalize(contract["props"])
|
|
45
|
+
slots = normalize(contract["slots"])
|
|
46
|
+
given = Array(prop_names).map(&:to_s)
|
|
47
|
+
where = { component: component_name, file: at[:file], line: at[:line], snippet: at[:snippet] }
|
|
48
|
+
unknown = contract["passthrough"] == true ? [] : (given - (props.keys + slots.keys))
|
|
49
|
+
|
|
50
|
+
# Priority order so the highest-signal message wins. A near-miss typo is
|
|
51
|
+
# reported FIRST even when it leaves a required prop "missing" — e.g.
|
|
52
|
+
# `<LikeButton postID={1} />` (postID typo of the required postId) should
|
|
53
|
+
# say "unknown prop \"postID\" — did you mean \"postId\"?", not the less
|
|
54
|
+
# helpful "missing required prop \"postId\"" (FR100's canonical typo case).
|
|
55
|
+
check_typo(unknown, props.keys + slots.keys, where: where)
|
|
56
|
+
check_missing_required(props, given, kind: "prop", where: where)
|
|
57
|
+
check_missing_required(slots, given, kind: "slot", where: where)
|
|
58
|
+
check_unknown(unknown.first, where: where) if unknown.any?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Normalize a contract sub-section into a { "name" => "required"|"optional" }
|
|
62
|
+
# Hash. Accepts the manifest's Hash form, an Array (all optional — the JS
|
|
63
|
+
# extractor already collapses array slots to a Hash, but be defensive), or
|
|
64
|
+
# nil/garbage (→ {}).
|
|
65
|
+
def self.normalize(section)
|
|
66
|
+
case section
|
|
67
|
+
when Hash then section
|
|
68
|
+
when Array then section.to_h { |name| [name.to_s, "optional"] }
|
|
69
|
+
else {}
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# An unknown prop whose closest declared match is within edit distance is a
|
|
74
|
+
# likely typo — report it (with the suggestion) ahead of any missing-required
|
|
75
|
+
# message. Returns/raises on the first such prop; a no-op when none qualify.
|
|
76
|
+
def self.check_typo(unknown, declared, where:)
|
|
77
|
+
unknown.each do |name|
|
|
78
|
+
suggestion = StringDistance.closest_match(name, declared)
|
|
79
|
+
next unless suggestion
|
|
80
|
+
|
|
81
|
+
raise_error("got unknown prop #{name.inspect} — did you mean #{suggestion.inspect}?", where)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.check_missing_required(spec, given, kind:, where:)
|
|
86
|
+
required = spec.select { |_, requiredness| requiredness == "required" }.keys
|
|
87
|
+
missing = required - given
|
|
88
|
+
return if missing.empty?
|
|
89
|
+
|
|
90
|
+
name = missing.first
|
|
91
|
+
raise_error("is missing required #{kind} #{name.inspect} — add the required #{kind} #{name.inspect}", where)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.check_unknown(name, where:)
|
|
95
|
+
raise_error("got unknown prop #{name.inspect}", where)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Build + raise the {Ruact::ComponentContractError} with the full message:
|
|
99
|
+
# component name + file:line + the offending prop/slot + the corrective
|
|
100
|
+
# suggestion. The message is self-contained (the preprocessor re-raises it
|
|
101
|
+
# as-is, no generic line/snippet tail appended).
|
|
102
|
+
#
|
|
103
|
+
# @param detail [String] the violation clause
|
|
104
|
+
# @param where [Hash] keys +component+, +file+, +line+, +snippet+
|
|
105
|
+
def self.raise_error(detail, where)
|
|
106
|
+
location = [where[:file], where[:line]].compact.join(":")
|
|
107
|
+
location = "(unknown location)" if location.empty?
|
|
108
|
+
message = "ruact: <#{where[:component]}> at #{location} #{detail}."
|
|
109
|
+
message += "\n in: #{where[:snippet]}" if where[:snippet] && !where[:snippet].empty?
|
|
110
|
+
raise ComponentContractError, message
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private_class_method :normalize, :check_typo, :check_missing_required, :check_unknown, :raise_error
|
|
114
|
+
end
|
|
115
|
+
end
|
data/lib/ruact/configuration.rb
CHANGED
|
@@ -17,11 +17,13 @@ module Ruact
|
|
|
17
17
|
strict_serialization
|
|
18
18
|
suspense_timeout
|
|
19
19
|
vite_dev_server
|
|
20
|
-
current_user_resolver
|
|
21
20
|
dev_error_payload_enabled
|
|
22
21
|
max_upload_bytes
|
|
23
22
|
query_route_prefix
|
|
24
23
|
query_parent_controller
|
|
24
|
+
signed_global_id_default_purpose
|
|
25
|
+
signed_global_id_default_expires_in
|
|
26
|
+
shadcn_compatible_versions
|
|
25
27
|
].freeze
|
|
26
28
|
|
|
27
29
|
# @!attribute [r] manifest_path
|
|
@@ -38,17 +40,6 @@ module Ruact
|
|
|
38
40
|
# @!attribute [r] vite_dev_server
|
|
39
41
|
# @return [String] Base URL of the Vite dev server. Default: "http://localhost:5173".
|
|
40
42
|
#
|
|
41
|
-
# @!attribute [r] current_user_resolver
|
|
42
|
-
# @return [Proc, nil] Story 8.3 — Lambda invoked by the standalone
|
|
43
|
-
# server-action dispatcher when a block reads `current_user`. Receives
|
|
44
|
-
# `request.env` (Hash) and returns the authenticated user (or nil).
|
|
45
|
-
# Memoized per-dispatch; left nil by default so apps that don't use
|
|
46
|
-
# standalone actions never get a phantom `current_user` resolver.
|
|
47
|
-
# @example Devise
|
|
48
|
-
# Ruact.configure { |c| c.current_user_resolver = ->(env) { env['warden']&.user } }
|
|
49
|
-
# @example Hand-rolled session
|
|
50
|
-
# Ruact.configure { |c| c.current_user_resolver = ->(env) { User.find_by(id: env['rack.session'][:user_id]) } }
|
|
51
|
-
#
|
|
52
43
|
# @!attribute [r] dev_error_payload_enabled
|
|
53
44
|
# @return [Boolean, nil] Story 8.4 — When true, server-action failures
|
|
54
45
|
# respond with a verbose JSON payload (action name, error class,
|
|
@@ -56,8 +47,8 @@ module Ruact
|
|
|
56
47
|
# When false, the wire body carries only the four baseline fields
|
|
57
48
|
# (`_ruact_server_action_error`, `action_name`, `error_class`,
|
|
58
49
|
# `message`) so React components can render their own UI without
|
|
59
|
-
# accidental backtrace leakage. Default `nil` — the
|
|
60
|
-
#
|
|
50
|
+
# accidental backtrace leakage. Default `nil` — the error-rendering
|
|
51
|
+
# layer resolves nil to `Rails.env.development? || Rails.env.test?`,
|
|
61
52
|
# keeping the Configuration trivially constructible in non-Rails specs.
|
|
62
53
|
# @example Force production-shape errors in development
|
|
63
54
|
# Ruact.configure { |c| c.dev_error_payload_enabled = false }
|
|
@@ -65,9 +56,9 @@ module Ruact
|
|
|
65
56
|
# @!attribute [r] max_upload_bytes
|
|
66
57
|
# @return [Integer, nil] Story 8.5 — upper bound (in bytes) on the
|
|
67
58
|
# `Content-Length` of `multipart/form-data` and
|
|
68
|
-
# `application/x-www-form-urlencoded` requests dispatched
|
|
69
|
-
# `
|
|
70
|
-
# this value, the
|
|
59
|
+
# `application/x-www-form-urlencoded` requests dispatched to a
|
|
60
|
+
# `Ruact::Server` mutation route. When the inbound `Content-Length`
|
|
61
|
+
# exceeds this value, the server concern raises
|
|
71
62
|
# `Ruact::UploadTooLargeError` BEFORE Rack's multipart parser runs,
|
|
72
63
|
# producing a 413 with the Story 8.4 structured error body.
|
|
73
64
|
# Default: `10 * 1024 * 1024` (10 MB). Set to `nil` to disable the
|
|
@@ -105,6 +96,45 @@ module Ruact
|
|
|
105
96
|
# scoping, Pundit) runs before any query class is instantiated (FR89).
|
|
106
97
|
# @example Dispatch queries through an API base controller
|
|
107
98
|
# Ruact.configure { |c| c.query_parent_controller = "Api::BaseController" }
|
|
99
|
+
#
|
|
100
|
+
# @!attribute [r] signed_global_id_default_purpose
|
|
101
|
+
# @return [Symbol, String, nil] Story 13.2 (FR96) — the default `for:`
|
|
102
|
+
# purpose `Ruact.signed_global_id` / `Ruact.locate_signed` use when the
|
|
103
|
+
# call omits `for:`. A purpose scopes a signed reference to one
|
|
104
|
+
# use-site so a token minted for editing a post cannot be replayed
|
|
105
|
+
# against, say, a delete endpoint. Default `nil` — when neither the
|
|
106
|
+
# call nor this config supplies a purpose, the helper raises
|
|
107
|
+
# `Ruact::Error` rather than sign an unscoped token (the "developer
|
|
108
|
+
# forgot" path is a loud error, never a silent insecure default).
|
|
109
|
+
# Prefer a per-call `for:` when use-sites differ; set this only for an
|
|
110
|
+
# app-wide default purpose.
|
|
111
|
+
# @example Set an app-wide default purpose
|
|
112
|
+
# Ruact.configure { |c| c.signed_global_id_default_purpose = :ruact_ref }
|
|
113
|
+
#
|
|
114
|
+
# @!attribute [r] signed_global_id_default_expires_in
|
|
115
|
+
# @return [ActiveSupport::Duration, nil] Story 13.2 (FR96) — the default
|
|
116
|
+
# `expires_in:` `Ruact.signed_global_id` uses when the call omits
|
|
117
|
+
# `expires_in:`. Must be an `ActiveSupport::Duration` (e.g. `15.minutes`)
|
|
118
|
+
# — globalid calls `#from_now` on it. Default `nil` — when neither the
|
|
119
|
+
# call nor this config supplies an expiry, the helper raises
|
|
120
|
+
# `Ruact::Error` rather than mint a non-expiring token. To deliberately
|
|
121
|
+
# mint a non-expiring token, pass an explicit `expires_in: nil` at the
|
|
122
|
+
# call site (a reviewed per-call choice), never via this default.
|
|
123
|
+
# @example Set an app-wide default expiry
|
|
124
|
+
# Ruact.configure { |c| c.signed_global_id_default_expires_in = 1.hour }
|
|
125
|
+
#
|
|
126
|
+
# @!attribute [r] shadcn_compatible_versions
|
|
127
|
+
# @return [Array<Integer>] Story 10.5 — the shadcn/ui MAJOR versions the
|
|
128
|
+
# `ruact:scaffold` generator is regression-tested against. When the
|
|
129
|
+
# generator detects an installed shadcn major (best-effort, from the
|
|
130
|
+
# host `package.json`) that is NOT in this list, it emits a warning
|
|
131
|
+
# (never a hard stop) that the generated components may import from
|
|
132
|
+
# outdated `@/components/ui/*` paths. Must be a non-empty Array of
|
|
133
|
+
# Integers. Default `[1, 2]` (the majors tested at gem-release time).
|
|
134
|
+
# A dev who has manually verified a newer major adds it here to
|
|
135
|
+
# suppress the warning — the documented "override" path.
|
|
136
|
+
# @example Allow shadcn v3 once you have verified it
|
|
137
|
+
# Ruact.configure { |c| c.shadcn_compatible_versions = [1, 2, 3] }
|
|
108
138
|
ATTRIBUTES.each do |attr|
|
|
109
139
|
attr_reader attr
|
|
110
140
|
|
|
@@ -135,11 +165,10 @@ module Ruact
|
|
|
135
165
|
if template
|
|
136
166
|
ATTRIBUTES.each do |attr|
|
|
137
167
|
value = template.public_send(attr)
|
|
138
|
-
# Procs are immutable from the outside
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
# at seal! time is enough.
|
|
168
|
+
# Procs are immutable from the outside. Duping creates a different Proc
|
|
169
|
+
# instance, breaking identity comparisons across re-configurations.
|
|
170
|
+
# Procs are inherently re-entrant safe (no mutable internal state
|
|
171
|
+
# surface) so the dup is unnecessary; the freeze at seal! time is enough.
|
|
143
172
|
cloned = value.is_a?(Proc) ? value : value.dup
|
|
144
173
|
instance_variable_set("@#{attr}", cloned)
|
|
145
174
|
end
|
|
@@ -152,11 +181,13 @@ module Ruact
|
|
|
152
181
|
end
|
|
153
182
|
@suspense_timeout = 5.0
|
|
154
183
|
@vite_dev_server = "http://localhost:5173"
|
|
155
|
-
@current_user_resolver = nil
|
|
156
184
|
@dev_error_payload_enabled = nil
|
|
157
185
|
@max_upload_bytes = 10 * 1024 * 1024
|
|
158
186
|
@query_route_prefix = "/q"
|
|
159
187
|
@query_parent_controller = "ApplicationController"
|
|
188
|
+
@signed_global_id_default_purpose = nil
|
|
189
|
+
@signed_global_id_default_expires_in = nil
|
|
190
|
+
@shadcn_compatible_versions = [1, 2]
|
|
160
191
|
end
|
|
161
192
|
end
|
|
162
193
|
|
|
@@ -186,11 +217,10 @@ module Ruact
|
|
|
186
217
|
|
|
187
218
|
# Story 8.3 review — Procs CAN be frozen (`.freeze` flips the frozen
|
|
188
219
|
# flag; no operational effect on `.call`). Freezing in place preserves
|
|
189
|
-
# object identity (vital for code that compares
|
|
190
|
-
# re-configurations) AND keeps the deep-freeze contract honest —
|
|
191
|
-
#
|
|
192
|
-
#
|
|
193
|
-
# `frozen?` would see the right answer.
|
|
220
|
+
# object identity (vital for code that compares a Proc-valued attribute
|
|
221
|
+
# across re-configurations) AND keeps the deep-freeze contract honest —
|
|
222
|
+
# a later `.freeze` would otherwise silently no-op an already-frozen
|
|
223
|
+
# reference, but a caller probing `frozen?` would see the right answer.
|
|
194
224
|
if value.is_a?(Proc)
|
|
195
225
|
value.freeze
|
|
196
226
|
else
|
|
@@ -218,6 +248,7 @@ module Ruact
|
|
|
218
248
|
when :max_upload_bytes then validate_max_upload_bytes!(value)
|
|
219
249
|
when :query_route_prefix then validate_query_route_prefix!(value)
|
|
220
250
|
when :query_parent_controller then validate_query_parent_controller!(value)
|
|
251
|
+
when :shadcn_compatible_versions then validate_shadcn_compatible_versions!(value)
|
|
221
252
|
end
|
|
222
253
|
end
|
|
223
254
|
|
|
@@ -261,6 +292,27 @@ module Ruact
|
|
|
261
292
|
"got #{value.inspect} (#{value.class.name})."
|
|
262
293
|
end
|
|
263
294
|
|
|
295
|
+
# Story 10.5 — the scaffold generator validates the detected shadcn major
|
|
296
|
+
# against this list (`Array#include?`), so a non-Array would raise at the
|
|
297
|
+
# first scaffold run instead of at boot, and an empty Array would make every
|
|
298
|
+
# detected major "incompatible" (a global false warning). Both are
|
|
299
|
+
# configuration-time errors. Element-type strictness (Integer-only majors)
|
|
300
|
+
# is enforced too — a `["2"]` typo would silently never match a detected
|
|
301
|
+
# Integer major and warn on every run.
|
|
302
|
+
def validate_shadcn_compatible_versions!(value)
|
|
303
|
+
unless value.is_a?(Array) && !value.empty?
|
|
304
|
+
raise Ruact::ConfigurationError,
|
|
305
|
+
"Ruact::Configuration#shadcn_compatible_versions must be a non-empty Array " \
|
|
306
|
+
"of major-version Integers (e.g. [1, 2]); " \
|
|
307
|
+
"got #{value.inspect} (#{value.class.name})."
|
|
308
|
+
end
|
|
309
|
+
return if value.all?(Integer)
|
|
310
|
+
|
|
311
|
+
raise Ruact::ConfigurationError,
|
|
312
|
+
"Ruact::Configuration#shadcn_compatible_versions must contain only Integer " \
|
|
313
|
+
"major versions (e.g. [1, 2]); got #{value.inspect}."
|
|
314
|
+
end
|
|
315
|
+
|
|
264
316
|
def build_error_message(attr, location)
|
|
265
317
|
<<~MSG.strip
|
|
266
318
|
ruact: cannot mutate Ruact::Configuration##{attr} after initialization.
|