Module: Ruact::ServerFunctions::NameBridge

Defined in:
lib/ruact/server_functions/name_bridge.rb

Overview

Translates a Ruby symbol into the JS identifier exported from app/javascript/.ruact/server-functions.ts. The bridge is Ruby-side only; the Vite plugin reads the already-translated identifier from the JSON snapshot and emits it verbatim (Story 8.0a design decision: one source of truth for naming).

Rules (locked by Story 8.0 ADR + 2026-05-13 review-patch tightening):

  • Symbol must match /\A[a-z_][a-z0-9_]*\z/; otherwise raises ConfigurationError at controller-class load time.
  • A single leading underscore is preserved (e.g. :_internal_dump"_internalDump").
  • Runs of underscores collapse and uppercase the following alphanumeric (e.g. :foo__bar"fooBar").
  • The source symbol cannot be made of underscores alone (:_, :__, …) — would otherwise emit "_", which carries no semantic content and collides with the common "ignored value" lint convention.
  • The translated JS identifier cannot match a JavaScript reserved word or future-reserved word (ES2020+ list plus the module-top-level await / async). :class"class" would compile under Babel loose-mode but break under tsc --noEmit and most ESLint configs.

See Also:

  • "Naming bridge"

Constant Summary collapse

VALID_SYMBOL =
/\A[a-z_][a-z0-9_]*\z/
UNDERSCORE_ONLY =
/\A_+\z/
RESERVED_JS_IDENTIFIERS =

ES2020+ reserved + strict-mode reserved + contextually-reserved at module top level + strict-mode invalid binding names. The codegen emits in a module context (the generated app/javascript/.ruact/server-functions.ts is "type": "module", so all code runs in strict mode), so await, eval, and arguments are all reserved as identifier names. Keep this list sorted; matches the MDN reference plus the contextual additions and the strict-mode eval/arguments ban from the 2026-05-13 Re-run review patch.

%w[
  arguments async await break case catch class const continue
  debugger default delete do else enum eval export extends false
  finally for function if implements import in instanceof interface
  let new null package private protected public return static super
  switch this throw true try typeof var void while with yield
].to_set.freeze
RESERVED_BY_RUACT =

Story 8.2 (2026-05-17 review patches R2 + R12) — names already bound at the top of app/javascript/.ruact/server-functions.ts, either by a helper re-export (revalidate, useQuery) or a runtime import (_makeServerFunction, _makeQuery). A server function / query method named revalidate (or use_query) would emit a clashing export const / duplicate export next to the existing binding and crash the generated module at load time. The rule fires at route-draw so the failure surfaces during boot, not at first request. Story 9.5 added _makeQuery (the v2 query import) and useQuery (the query hook re-export) to this list. Story 9.9 removed _makeRef (the demolished v1 runtime export).

%w[
  _makeQuery
  _makeServerFunction
  revalidate
  useQuery
].to_set.freeze

Class Method Summary collapse

Class Method Details

.to_js_identifier(symbol) ⇒ String

Returns the corresponding JS identifier.

Examples:

Ruact::ServerFunctions::NameBridge.to_js_identifier(:create_post)
# => "createPost"

leading underscore preserved

Ruact::ServerFunctions::NameBridge.to_js_identifier(:_internal_dump)
# => "_internalDump"

Parameters:

  • symbol (Symbol, String)

    the Ruby identifier of a routed server function (a mutation action name or a Ruact::Query method name).

Returns:

  • (String)

    the corresponding JS identifier.

Raises:

  • (Ruact::ConfigurationError)

    when symbol does not match the allowed shape, is all-underscores, or maps to a JS reserved word — caught at route-draw time so misnamed routes never reach production.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/ruact/server_functions/name_bridge.rb', line 79

def to_js_identifier(symbol)
  str = symbol.to_s

  unless str.match?(VALID_SYMBOL)
    raise Ruact::ConfigurationError,
          "ruact server-function name :#{symbol} must match /^[a-z_][a-z0-9_]*$/"
  end

  if str.match?(UNDERSCORE_ONLY)
    raise Ruact::ConfigurationError,
          "ruact server-function name :#{symbol} cannot be composed " \
          "entirely of underscores (no semantic content)"
  end

  leading = str.start_with?("_") ? "_" : ""
  body    = str.sub(/\A_+/, "")
  js_id   = leading + body.gsub(/_+([a-z0-9])/) { Regexp.last_match(1).upcase }

  if RESERVED_JS_IDENTIFIERS.include?(js_id)
    raise Ruact::ConfigurationError,
          "ruact server-function name :#{symbol} maps to JS reserved " \
          "word \"#{js_id}\" — pick a different name (e.g. :#{symbol}_fn)"
  end

  if RESERVED_BY_RUACT.include?(js_id)
    raise Ruact::ConfigurationError,
          "ruact server-function name :#{symbol} maps to \"#{js_id}\", " \
          "which is already exported by the ruact runtime from " \
          "`@/.ruact/server-functions` and would emit a duplicate export. " \
          "Pick a different name (e.g. :#{symbol}_fn)."
  end

  js_id
end