Class: Ruact::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/ruact/configuration.rb

Overview

Holds gem-wide configuration. Instantiated once via Ruact.config. Configure via Ruact.configure { |c| c.attr = value } in an initializer.

Frozen after Ruact.configure returns (Story 7.3) — direct post-boot mutation (Ruact.config.attr = value outside the block) raises Ruact::ConfigurationError with the offending attribute, the caller's file:line, and the suggested fix. Re-calling Ruact.configure after boot replaces the configuration atomically and emits a [ruact] warning.

Constant Summary collapse

ATTRIBUTES =

The set of public attributes; new attributes added here automatically inherit the freeze contract via the define_method writer below.

%i[
  manifest_path
  strict_serialization
  suspense_timeout
  vite_dev_server
  dev_error_payload_enabled
  max_upload_bytes
  query_route_prefix
  query_parent_controller
  signed_global_id_default_purpose
  signed_global_id_default_expires_in
  shadcn_compatible_versions
  layout
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template: nil) ⇒ Configuration

Build a fresh Configuration. When template is given, dup every public attribute from it so the draft is mutable — used by Ruact.configure for atomic-replacement cloning. The dup is required because the template is always a published (frozen) Configuration with deep-frozen attribute values, and AC1 requires the DSL inside the configure block to behave identically regardless of whether this is the first call or a later one (including idiomatic in-place mutation of inherited values).

dup is safe for every supported attribute type: Strings produce an unfrozen copy; nil/true/false/Numerics/Symbols dup to themselves (they are inherently immutable, so the dup is a no-op).

Parameters:



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/ruact/configuration.rb', line 206

def initialize(template: nil)
  if template
    ATTRIBUTES.each do |attr|
      value = template.public_send(attr)
      # Procs are immutable from the outside. Duping creates a different Proc
      # instance, breaking identity comparisons across re-configurations.
      # Procs are inherently re-entrant safe (no mutable internal state
      # surface) so the dup is unnecessary; the freeze at seal! time is enough.
      cloned = value.is_a?(Proc) ? value : value.dup
      instance_variable_set("@#{attr}", cloned)
    end
  else
    @manifest_path        = nil
    @strict_serialization = begin
      Rails.env.production?
    rescue StandardError
      false
    end
    @suspense_timeout     = 5.0
    @vite_dev_server      = "http://localhost:5173"
    @dev_error_payload_enabled = nil
    @max_upload_bytes = 10 * 1024 * 1024
    @query_route_prefix = "/q"
    @query_parent_controller = "ApplicationController"
    @signed_global_id_default_purpose = nil
    @signed_global_id_default_expires_in = nil
    @shadcn_compatible_versions = [1, 2]
    @layout = false
  end
end

Instance Attribute Details

#dev_error_payload_enabledBoolean? (readonly)

Returns Story 8.4 — When true, server-action failures respond with a verbose JSON payload (action name, error class, message, split backtrace, contextual suggestion, validation errors). When false, the wire body carries only the four baseline fields (_ruact_server_action_error, action_name, error_class, message) so React components can render their own UI without accidental backtrace leakage. Default nil — the error-rendering layer resolves nil to Rails.env.development? || Rails.env.test?, keeping the Configuration trivially constructible in non-Rails specs.

Examples:

Force production-shape errors in development

Ruact.configure { |c| c.dev_error_payload_enabled = false }

Returns:

  • (Boolean, nil)

    Story 8.4 — When true, server-action failures respond with a verbose JSON payload (action name, error class, message, split backtrace, contextual suggestion, validation errors). When false, the wire body carries only the four baseline fields (_ruact_server_action_error, action_name, error_class, message) so React components can render their own UI without accidental backtrace leakage. Default nil — the error-rendering layer resolves nil to Rails.env.development? || Rails.env.test?, keeping the Configuration trivially constructible in non-Rails specs.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#layoutBoolean, String (readonly)

Note:

A ruact view is rendered in its own pass (it produces the component tree), so content_for declared inside the view does NOT reach the layout. Set document metadata from the controller instead.

Returns Which document wrapper a ruact page's HTML response is rendered into. The Flight response shape (text/x-component) is never affected — this is only about the full-document render a browser gets on a normal navigation.

  • false (default) — render the gem's built-in minimal shell.
  • true — render through the controller's normal Rails layout.
  • a String — render through that named layout (e.g. "ruact").

The layout path exists because the document <head> belongs to the host app: stylesheet_link_tag, favicons, fonts, analytics and any <head>-writing gem only reach the page when Rails' own layout owns the document. The built-in shell carries no stylesheet slot, so under the false default a ruact page renders with no app CSS at all — which is why rails generate ruact:install writes config.layout = true into the generated initializer and adds <%= ruact_js_assets %> to your layout in the same run.

This setting is deliberately explicit — there is no auto-detection. ruact used to try to infer whether your layout was ready by inspecting it. Deciding that reliably means answering "does this template call this method?", which cannot be done by pattern-matching a template language: three review rounds each found another shape that fooled it (a mention in a comment, a commented-out call, a trim-mode comment), and each wrong answer governed how every page in the app rendered. One explicit line is worth more than a clever guess here.

A layout is ready when it calls <%= ruact_js_assets %> (which emits the React root's bootstrap entry tags and the per-render Flight payload) next to a <div id="root"></div>. If it does not, ruact says so loudly in development rather than serving a blank page, and rails ruact:doctor reports it.

Examples:

Let your layout own the document (what ruact:install writes)

Ruact.configure { |c| c.layout = true }

Use a dedicated layout for ruact pages only

Ruact.configure { |c| c.layout = "ruact" }

Returns:

  • (Boolean, String)

    Which document wrapper a ruact page's HTML response is rendered into. The Flight response shape (text/x-component) is never affected — this is only about the full-document render a browser gets on a normal navigation.

    • false (default) — render the gem's built-in minimal shell.
    • true — render through the controller's normal Rails layout.
    • a String — render through that named layout (e.g. "ruact").

    The layout path exists because the document <head> belongs to the host app: stylesheet_link_tag, favicons, fonts, analytics and any <head>-writing gem only reach the page when Rails' own layout owns the document. The built-in shell carries no stylesheet slot, so under the false default a ruact page renders with no app CSS at all — which is why rails generate ruact:install writes config.layout = true into the generated initializer and adds <%= ruact_js_assets %> to your layout in the same run.

    This setting is deliberately explicit — there is no auto-detection. ruact used to try to infer whether your layout was ready by inspecting it. Deciding that reliably means answering "does this template call this method?", which cannot be done by pattern-matching a template language: three review rounds each found another shape that fooled it (a mention in a comment, a commented-out call, a trim-mode comment), and each wrong answer governed how every page in the app rendered. One explicit line is worth more than a clever guess here.

    A layout is ready when it calls <%= ruact_js_assets %> (which emits the React root's bootstrap entry tags and the per-render Flight payload) next to a <div id="root"></div>. If it does not, ruact says so loudly in development rather than serving a blank page, and rails ruact:doctor reports it.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#manifest_pathString? (readonly)

Returns Path to react-client-manifest.json. Defaults to Rails.root.join("public/react-client-manifest.json") when nil.

Returns:

  • (String, nil)

    Path to react-client-manifest.json. Defaults to Rails.root.join("public/react-client-manifest.json") when nil.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#max_upload_bytesInteger? (readonly)

Note:

This is a controller-level "fail fast at the boundary" knob, not a stream-safety guarantee — Rack's multipart parser will still buffer bodies up to its own limits before the guard rejects. For very large uploads route through Active Storage Direct Upload or a presigned S3 URL; see website/docs/api/server-actions.md "File uploads" section.

Returns Story 8.5 — upper bound (in bytes) on the Content-Length of multipart/form-data and application/x-www-form-urlencoded requests dispatched to a Ruact::Server mutation route. When the inbound Content-Length exceeds this value, the server concern raises Ruact::UploadTooLargeError BEFORE Rack's multipart parser runs, producing a 413 with the Story 8.4 structured error body. Default: 10 * 1024 * 1024 (10 MB). Set to nil to disable the gem-side guard — typical when a reverse proxy (client_max_body_size) or host middleware already owns the operational cap. Chunked-transfer requests (no Content-Length header) bypass the guard regardless of this setting; the action body is responsible for any belt-and-suspenders check via params[:file].size / params[:file].byte_size in that case.

Examples:

Raise the limit to 25 MB

Ruact.configure { |c| c.max_upload_bytes = 25 * 1024 * 1024 }

Disable the gem-side guard (reverse proxy owns the cap)

Ruact.configure { |c| c.max_upload_bytes = nil }

Returns:

  • (Integer, nil)

    Story 8.5 — upper bound (in bytes) on the Content-Length of multipart/form-data and application/x-www-form-urlencoded requests dispatched to a Ruact::Server mutation route. When the inbound Content-Length exceeds this value, the server concern raises Ruact::UploadTooLargeError BEFORE Rack's multipart parser runs, producing a 413 with the Story 8.4 structured error body. Default: 10 * 1024 * 1024 (10 MB). Set to nil to disable the gem-side guard — typical when a reverse proxy (client_max_body_size) or host middleware already owns the operational cap. Chunked-transfer requests (no Content-Length header) bypass the guard regardless of this setting; the action body is responsible for any belt-and-suspenders check via params[:file].size / params[:file].byte_size in that case.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#query_parent_controllerString (readonly)

Returns Story 9.4 — class NAME of the controller the gem's internal query dispatch controller inherits from (default "ApplicationController" — the Devise parent_controller pattern). Kept as a String and constantized lazily at route-draw time, NOT at configure time: ApplicationController does not exist when the gem loads. The host's REAL callback chain (authenticate_user!, tenant scoping, Pundit) runs before any query class is instantiated (FR89).

Examples:

Dispatch queries through an API base controller

Ruact.configure { |c| c.query_parent_controller = "Api::BaseController" }

Returns:

  • (String)

    Story 9.4 — class NAME of the controller the gem's internal query dispatch controller inherits from (default "ApplicationController" — the Devise parent_controller pattern). Kept as a String and constantized lazily at route-draw time, NOT at configure time: ApplicationController does not exist when the gem loads. The host's REAL callback chain (authenticate_user!, tenant scoping, Pundit) runs before any query class is instantiated (FR89).



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#query_route_prefixString (readonly)

Returns Story 9.4 — URL prefix under which the ruact_queries routing macro draws one named GET route per public query method (default "/q"GET /q/<jsIdentifier>). Must be a String starting with / and without a trailing slash (the macro joins prefix and identifier with /). Changing the prefix is configuration, never code.

Examples:

Mount queries under /api/queries

Ruact.configure { |c| c.query_route_prefix = "/api/queries" }

Returns:

  • (String)

    Story 9.4 — URL prefix under which the ruact_queries routing macro draws one named GET route per public query method (default "/q"GET /q/<jsIdentifier>). Must be a String starting with / and without a trailing slash (the macro joins prefix and identifier with /). Changing the prefix is configuration, never code.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#shadcn_compatible_versionsArray<Integer> (readonly)

Returns Story 10.5 — the shadcn/ui MAJOR versions the ruact:scaffold generator is regression-tested against. When the generator detects an installed shadcn major (best-effort, from the host package.json) that is NOT in this list, it emits a warning (never a hard stop) that the generated components may import from outdated @/components/ui/* paths. Must be a non-empty Array of Integers. Default [1, 2] (the majors tested at gem-release time). A dev who has manually verified a newer major adds it here to suppress the warning — the documented "override" path.

Examples:

Allow shadcn v3 once you have verified it

Ruact.configure { |c| c.shadcn_compatible_versions = [1, 2, 3] }

Returns:

  • (Array<Integer>)

    Story 10.5 — the shadcn/ui MAJOR versions the ruact:scaffold generator is regression-tested against. When the generator detects an installed shadcn major (best-effort, from the host package.json) that is NOT in this list, it emits a warning (never a hard stop) that the generated components may import from outdated @/components/ui/* paths. Must be a non-empty Array of Integers. Default [1, 2] (the majors tested at gem-release time). A dev who has manually verified a newer major adds it here to suppress the warning — the documented "override" path.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#signed_global_id_default_expires_inActiveSupport::Duration? (readonly)

Returns Story 13.2 (FR96) — the default expires_in: Ruact.signed_global_id uses when the call omits expires_in:. Must be an ActiveSupport::Duration (e.g. 15.minutes) — globalid calls #from_now on it. Default nil — when neither the call nor this config supplies an expiry, the helper raises Ruact::Error rather than mint a non-expiring token. To deliberately mint a non-expiring token, pass an explicit expires_in: nil at the call site (a reviewed per-call choice), never via this default.

Examples:

Set an app-wide default expiry

Ruact.configure { |c| c.signed_global_id_default_expires_in = 1.hour }

Returns:

  • (ActiveSupport::Duration, nil)

    Story 13.2 (FR96) — the default expires_in: Ruact.signed_global_id uses when the call omits expires_in:. Must be an ActiveSupport::Duration (e.g. 15.minutes) — globalid calls #from_now on it. Default nil — when neither the call nor this config supplies an expiry, the helper raises Ruact::Error rather than mint a non-expiring token. To deliberately mint a non-expiring token, pass an explicit expires_in: nil at the call site (a reviewed per-call choice), never via this default.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#signed_global_id_default_purposeSymbol, ... (readonly)

Returns Story 13.2 (FR96) — the default for: purpose Ruact.signed_global_id / Ruact.locate_signed use when the call omits for:. A purpose scopes a signed reference to one use-site so a token minted for editing a post cannot be replayed against, say, a delete endpoint. Default nil — when neither the call nor this config supplies a purpose, the helper raises Ruact::Error rather than sign an unscoped token (the "developer forgot" path is a loud error, never a silent insecure default). Prefer a per-call for: when use-sites differ; set this only for an app-wide default purpose.

Examples:

Set an app-wide default purpose

Ruact.configure { |c| c.signed_global_id_default_purpose = :ruact_ref }

Returns:

  • (Symbol, String, nil)

    Story 13.2 (FR96) — the default for: purpose Ruact.signed_global_id / Ruact.locate_signed use when the call omits for:. A purpose scopes a signed reference to one use-site so a token minted for editing a post cannot be replayed against, say, a delete endpoint. Default nil — when neither the call nor this config supplies a purpose, the helper raises Ruact::Error rather than sign an unscoped token (the "developer forgot" path is a loud error, never a silent insecure default). Prefer a per-call for: when use-sites differ; set this only for an app-wide default purpose.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#strict_serializationBoolean (readonly)

Returns When true, objects without explicit ruact_props declaration raise Ruact::SerializationError. Defaults to false in development, true in production.

Returns:

  • (Boolean)

    When true, objects without explicit ruact_props declaration raise Ruact::SerializationError. Defaults to false in development, true in production.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#suspense_timeoutFloat (readonly)

Returns Seconds before a deferred Suspense chunk times out. Default: 5.0.

Returns:

  • (Float)

    Seconds before a deferred Suspense chunk times out. Default: 5.0.



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end

#vite_dev_serverString (readonly)

Returns Base URL of the Vite dev server. Default: "http://localhost:5173&quot;.

Returns:



180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/ruact/configuration.rb', line 180

ATTRIBUTES.each do |attr|
  attr_reader attr

  define_method("#{attr}=") do |value|
    if frozen?
      location = caller_locations(1, 1).first
      raise Ruact::ConfigurationError, build_error_message(attr, location)
    end
    validate_attribute_value!(attr, value)
    instance_variable_set("@#{attr}", value)
  end
end