Module: Ruact::ServerFunctions::Codegen
- Defined in:
- lib/ruact/server_functions/codegen.rb,
lib/ruact/server_functions/codegen_v2.rb
Overview
Renders the snapshot Hash into the TypeScript module emitted to ‘app/javascript/.ruact/server-functions.ts`. Pure string-building plus a single write-if-changed call.
The output of Codegen.render MUST be byte-identical to the JS-side codegen in ‘gem/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs`. The cross-implementation parity test under `gem/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs` asserts this invariant; if it fails, fix the offending side rather than normalizing in the assertion (Story 8.0a Task 8.5).
Defined Under Namespace
Modules: V2
Constant Summary collapse
- VERSION =
Bumped only when the rendered shape changes. Used by tests to assert cross-implementation parity without coupling to the literal byte string.
1- VERSION_V2 =
Story 9.3 — the route-driven snapshot schema. A version-2 snapshot carries route-derived entries (‘http_method` + `path` + `segments`, no `ruby_symbol`) and renders `_makeServerFunction(descriptor)` calls instead of `_makeRef(“<sym>”)`. render dispatches on `version` so the v1 (registry-driven) path stays byte-for-byte untouched.
2- RUNTIME_IMPORT =
'"ruact/server-functions-runtime"'- ACTION_SIGNATURE =
Story 8.2 (2026-05-16, refined 2026-05-17 per review patch R1) —ACTION_SIGNATURE is a TS intersection type with TWO call signatures:
1. `(args?: FormData | Record<string, unknown>) => Promise<unknown>` — for direct callers (`await createPost({...})` / `await createPost(formData)` / event handlers), preserving the JSON-decoded response value. 2. `(formData: FormData) => Promise<void>` — assignable to `@types/react@19.x`'s `<form action>` prop, which is typed as `(formData: FormData) => void | Promise<void>`. TS rejects `Promise<unknown>` → `Promise<void>` even via the void-discard rule (Promise generics are invariant), so the intersection is required to make `<form action={createPost}>` typecheck DIRECTLY against the codegen-emitted module — no call-site cast, no wrapper closure.Runtime behavior is unchanged — ‘_makeRef` always resolves with the JSON-decoded value (or `null` for empty bodies). The intersection is a TYPE-ONLY surface: when callers `await` the result, they see `Promise<unknown>`; when React invokes the function from a `<form action>` prop, the `Promise<void>` overload is selected and the return value is discarded by React.
See the 2026-05-17 entry in ‘gem/docs/internal/decisions/server-functions-api.md` (“R1 — intersection-type refinement”) for the option (a)→(a′) evolution and the empirical typecheck-probe that motivated it. Query signatures stay narrow because queries are never reachable via `<form action>` (read-only via `useQuery`).
"((args?: FormData | Record<string, unknown>) => Promise<unknown>) " \ "& ((formData: FormData) => Promise<void>)"
- QUERY_SIGNATURE =
"() => Promise<unknown>"- REVALIDATE_REEXPORT =
Story 8.2 — fixed re-export appended AFTER the per-function block. Emitted in BOTH branches (empty + populated registry) so ‘import { revalidate } from “@/.ruact/server-functions”` works on day one of any host app. Ruby + JS codegens emit byte-identically.
"export { revalidate } from #{RUNTIME_IMPORT};\n".freeze
- VALID_JS_IDENTIFIER =
JS identifier shape — same as ‘NameBridge::VALID_SYMBOL` but expressed in JS-identifier terms (leading letter / underscore / `$`, then alnum / underscore / `$`). The codegen validates every entry it consumes because the JSON bridge is a trust boundary — a malformed snapshot (`functions[].js_identifier == “);nevil();_makeRef(”foo`) would otherwise inject TS at module top level.
/\A[A-Za-z_$][A-Za-z0-9_$]*\z/- ALLOWED_KINDS =
%w[action query].freeze
- LINE_TERMINATORS =
JS comments (both ‘//` line comments and `/* … */` block comments via the spec’s LineTerminator production) end on LF, CR, U+2028, and U+2029. A snapshot value that smuggles any of these would break out of the leading comment header in the emitted module. The reviewer’s Pass-2 finding noted that an earlier ‘/[rn]/` guard missed the two Unicode line separators; the regex is widened here and a parity test in `server-functions-codegen.test.mjs` keeps both renderers in sync.
/[\r\n ]/
Class Method Summary collapse
-
.generate_ts!(snapshot:, output_path:) ⇒ Boolean
Writes the rendered TS module to
output_path, only if it changed. -
.render(snapshot) ⇒ String
Renders
snapshotinto the TS module text.
Class Method Details
.generate_ts!(snapshot:, output_path:) ⇒ Boolean
Writes the rendered TS module to output_path, only if it changed. See SnapshotWriter.write_if_changed!.
157 158 159 |
# File 'lib/ruact/server_functions/codegen.rb', line 157 def generate_ts!(snapshot:, output_path:) SnapshotWriter.write_if_changed!(path: output_path, content: render(snapshot)) end |
.render(snapshot) ⇒ String
Renders snapshot into the TS module text. Pure; no I/O.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/ruact/server_functions/codegen.rb', line 102 def render(snapshot) unless snapshot.is_a?(Hash) raise Ruact::ConfigurationError, "ruact server-function codegen: snapshot must be a Hash, got #{snapshot.class}" end version = fetch_snapshot_key!(snapshot, :version, "version") generated_at = fetch_snapshot_key!(snapshot, :generated_at, "generated_at") functions = fetch_snapshot_key!(snapshot, :functions, "functions") (version, generated_at) return V2.render(version, generated_at, functions) if version.to_s == VERSION_V2.to_s validate_functions!(functions) io = +"" io << "// AUTO-GENERATED by vite-plugin-ruact (Story 8.0a). DO NOT EDIT.\n" io << "// Source: tmp/cache/ruact/server-functions.json (version #{version})\n" io << "// Generated at: #{generated_at}\n" io << "import { _makeRef } from #{RUNTIME_IMPORT};\n" if functions.empty? io << "\n// (no server functions registered yet — Stories 8.1 / 9.1 populate)\n" # `noUnusedLocals` would otherwise flag the `_makeRef` import. The # `void` discard pattern keeps the import alive at zero runtime # cost; once an action/query is registered the export below # references `_makeRef` directly and this line is omitted. io << "void _makeRef;\n" else io << "\n" functions.each do |entry| io << render_export(entry) end end # Story 8.2 — `revalidate()` is always available, so the # re-export lands in both branches (empty registry + populated). # The codegen owns the canonical import path # `@/.ruact/server-functions` and is the only stable surface devs # are told to import from in the docs (per the Story 8.0 ADR); # without this line, devs would need a second import statement # from a less-stable runtime-package path. io << "\n" io << REVALIDATE_REEXPORT io end |