ruact 0.0.5 → 0.0.7
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 +74 -18
- 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 +79 -425
- data/lib/ruact/doctor.rb +133 -4
- data/lib/ruact/erb_preprocessor.rb +71 -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/manifest_resolver.rb +149 -0
- data/lib/ruact/railtie.rb +70 -203
- data/lib/ruact/render_pipeline.rb +8 -1
- 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 +109 -0
- data/lib/ruact.rb +20 -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 +70 -328
- 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/manifest_resolver_spec.rb +159 -0
- 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 +15 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +40 -45
- data/vendor/javascript/ruact-server-functions-runtime/index.js +42 -98
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +72 -75
- data/vendor/javascript/ruact-server-functions-runtime/package.json +1 -1
- data/vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs +96 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +372 -6
- data/vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs +182 -0
- data/vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs +125 -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 +58 -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
|
@@ -49,30 +49,33 @@ const RESERVED_JS_IDENTIFIERS = new Set([
|
|
|
49
49
|
]);
|
|
50
50
|
|
|
51
51
|
// Story 8.2 R12 (2026-05-17) — names ALREADY bound at module top by the
|
|
52
|
-
// codegen itself: the runtime imports (`
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// Story 9.
|
|
52
|
+
// codegen itself: the runtime imports (`_makeServerFunction`, `_makeQuery`) and
|
|
53
|
+
// the re-exports (`revalidate`, `useQuery`). A snapshot that declared any as a
|
|
54
|
+
// `js_identifier` would emit a duplicate binding and crash at module-load time.
|
|
55
|
+
// Mirrors Ruby `NameBridge::RESERVED_BY_RUACT`. Story 9.5 added `_makeQuery` +
|
|
56
|
+
// `useQuery`; Story 9.9 removed the demolished v1 `_makeRef`.
|
|
57
57
|
const RESERVED_BY_RUACT = new Set([
|
|
58
58
|
"_makeQuery",
|
|
59
|
-
"_makeRef",
|
|
60
59
|
"_makeServerFunction",
|
|
61
60
|
"revalidate",
|
|
62
61
|
"useQuery",
|
|
63
62
|
]);
|
|
64
63
|
|
|
65
64
|
// Story 9.3 — the route-driven snapshot schema version + its verb allowlist.
|
|
66
|
-
// A version-2 snapshot renders `_makeServerFunction({...})` calls;
|
|
67
|
-
//
|
|
68
|
-
//
|
|
65
|
+
// A version-2 snapshot renders `_makeServerFunction({...})` calls; as of Story
|
|
66
|
+
// 9.9 it is the only supported version. Mirrors Ruby `Codegen::VERSION_V2` /
|
|
67
|
+
// `V2_HTTP_METHODS`.
|
|
69
68
|
export const VERSION_V2 = 2;
|
|
70
69
|
const V2_HTTP_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
71
|
-
// Story 9.5 — queries are GET-only (the
|
|
72
|
-
//
|
|
73
|
-
// `Codegen::V2::QUERY_HTTP_METHODS`.
|
|
70
|
+
// Story 9.5 — queries are GET-only (the 2026-06-02 ADR addendum restored HTTP
|
|
71
|
+
// GET semantics for queries). Mirrors Ruby `Codegen::V2::QUERY_HTTP_METHODS`.
|
|
74
72
|
const V2_QUERY_HTTP_METHODS = new Set(["GET"]);
|
|
75
73
|
|
|
74
|
+
// Story 13.4 — the VALUE type of every typed query param: the FR88 query-string
|
|
75
|
+
// wire union (keys + optionality are exact; per-param scalar precision is not
|
|
76
|
+
// reflection-honest and is deferred). Mirrors Ruby `Codegen::QUERY_PARAM_VALUE_TYPE`.
|
|
77
|
+
const QUERY_PARAM_VALUE_TYPE = "string | number | boolean | null";
|
|
78
|
+
|
|
76
79
|
/**
|
|
77
80
|
* Absolute path to the placeholder runtime bundled inside the gem. Used as the
|
|
78
81
|
* target of the `ruact/server-functions-runtime` Vite alias so host apps
|
|
@@ -148,90 +151,21 @@ function validateMetadata(snapshot) {
|
|
|
148
151
|
}
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
function validateSnapshot(snapshot) {
|
|
152
|
-
validateMetadata(snapshot);
|
|
153
|
-
const { functions } = snapshot;
|
|
154
|
-
|
|
155
|
-
if (!Array.isArray(functions)) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`ruact server-function codegen: snapshot.functions must be an array, got ${typeof functions}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const seen = new Set();
|
|
162
|
-
for (const fn of functions) {
|
|
163
|
-
if (!fn || typeof fn !== "object" || Array.isArray(fn)) {
|
|
164
|
-
throw new Error(
|
|
165
|
-
`ruact server-function codegen: snapshot.functions entry is not an object: ${JSON.stringify(fn)}`,
|
|
166
|
-
);
|
|
167
|
-
}
|
|
168
|
-
if (typeof fn.ruby_symbol !== "string" || fn.ruby_symbol.length === 0) {
|
|
169
|
-
throw new Error(
|
|
170
|
-
"ruact server-function codegen: snapshot.functions entry has missing or " +
|
|
171
|
-
`empty ruby_symbol (got ${JSON.stringify(fn.ruby_symbol)}); the bridge ` +
|
|
172
|
-
"JSON is corrupted — regenerate via `bin/rails ruact:server_functions:generate`.",
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
if (fn.kind !== "action" && fn.kind !== "query") {
|
|
176
|
-
throw new Error(
|
|
177
|
-
"ruact server-function codegen: snapshot.functions entry has invalid kind " +
|
|
178
|
-
`${JSON.stringify(fn.kind)} (must be "action" or "query") for ` +
|
|
179
|
-
`ruby_symbol=${JSON.stringify(fn.ruby_symbol)}`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
if (typeof fn.js_identifier !== "string" || !VALID_JS_IDENTIFIER.test(fn.js_identifier)) {
|
|
183
|
-
throw new Error(
|
|
184
|
-
"ruact server-function codegen rejected a snapshot entry: " +
|
|
185
|
-
`ruby_symbol=${JSON.stringify(fn.ruby_symbol)} ` +
|
|
186
|
-
`js_identifier=${JSON.stringify(fn.js_identifier)} is not a valid JS identifier ` +
|
|
187
|
-
"(must match /^[A-Za-z_$][A-Za-z0-9_$]*$/). The snapshot JSON is " +
|
|
188
|
-
"corrupted or was hand-edited — regenerate via " +
|
|
189
|
-
"`bin/rails ruact:server_functions:generate`.",
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
if (RESERVED_JS_IDENTIFIERS.has(fn.js_identifier)) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
`ruact server-function codegen: js_identifier "${fn.js_identifier}" is a reserved ` +
|
|
195
|
-
`JS word — ruby_symbol=${JSON.stringify(fn.ruby_symbol)} would emit an invalid ` +
|
|
196
|
-
"TS module. Ruby NameBridge should have rejected this; regenerate via " +
|
|
197
|
-
"`bin/rails ruact:server_functions:generate`.",
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
if (RESERVED_BY_RUACT.has(fn.js_identifier)) {
|
|
201
|
-
throw new Error(
|
|
202
|
-
`ruact server-function codegen: js_identifier "${fn.js_identifier}" is reserved by ` +
|
|
203
|
-
"the ruact runtime/codegen surface (would clash with the module's `revalidate` " +
|
|
204
|
-
`re-export or \`_makeRef\` import) — ruby_symbol=${JSON.stringify(fn.ruby_symbol)} ` +
|
|
205
|
-
"cannot be exported. NameBridge should have rejected this; regenerate via " +
|
|
206
|
-
"`bin/rails ruact:server_functions:generate`.",
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
if (seen.has(fn.js_identifier)) {
|
|
210
|
-
throw new Error(
|
|
211
|
-
`ruact server-function codegen: duplicate js_identifier "${fn.js_identifier}" in ` +
|
|
212
|
-
"snapshot — two entries would emit conflicting `export const` declarations. " +
|
|
213
|
-
"The snapshot JSON is corrupted or was hand-edited — regenerate via " +
|
|
214
|
-
"`bin/rails ruact:server_functions:generate`.",
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
seen.add(fn.js_identifier);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
154
|
/**
|
|
222
|
-
* Renders the snapshot Hash into the TS module text.
|
|
223
|
-
* to {Ruact::ServerFunctions::Codegen.render}.
|
|
155
|
+
* Renders the route-driven (version-2) snapshot Hash into the TS module text.
|
|
156
|
+
* MUST stay byte-identical to {Ruact::ServerFunctions::Codegen.render}. Story
|
|
157
|
+
* 9.9 demolished the v1 (registry / `_makeRef`) render path; only version 2 is
|
|
158
|
+
* supported.
|
|
224
159
|
*
|
|
225
160
|
* @param {{ version: number, generated_at: string, functions: Array<{
|
|
226
|
-
*
|
|
161
|
+
* js_identifier: string, kind: string, http_method?: string, path?: string,
|
|
162
|
+
* segments?: string[]
|
|
227
163
|
* }> }} snapshot
|
|
228
164
|
* @returns {string}
|
|
229
165
|
*/
|
|
230
166
|
export function render(snapshot) {
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
// untouched. The version peek is shape-guarded so a corrupt non-object
|
|
234
|
-
// snapshot still falls through to validateSnapshot's loud failure.
|
|
167
|
+
// The version peek is shape-guarded so a corrupt non-object snapshot still
|
|
168
|
+
// falls through to the loud validation failure below.
|
|
235
169
|
if (
|
|
236
170
|
snapshot &&
|
|
237
171
|
typeof snapshot === "object" &&
|
|
@@ -241,34 +175,15 @@ export function render(snapshot) {
|
|
|
241
175
|
return renderV2(snapshot);
|
|
242
176
|
}
|
|
243
177
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
out += "\n// (no server functions registered yet — Stories 8.1 / 9.1 populate)\n";
|
|
254
|
-
// `noUnusedLocals` would otherwise flag the `_makeRef` import. The `void`
|
|
255
|
-
// discard pattern keeps the import alive at zero runtime cost; once an
|
|
256
|
-
// action / query is registered the export below references `_makeRef`
|
|
257
|
-
// directly and this line is omitted.
|
|
258
|
-
out += "void _makeRef;\n";
|
|
259
|
-
} else {
|
|
260
|
-
out += "\n";
|
|
261
|
-
for (const fn of functions) {
|
|
262
|
-
out += renderExport(fn);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Story 8.2 — `revalidate()` re-export. Emitted in both branches
|
|
266
|
-
// (empty + populated registry) because the helper is unconditional;
|
|
267
|
-
// see codegen.rb's REVALIDATE_REEXPORT and the 2026-05-16 Decision-log
|
|
268
|
-
// entry for the rationale.
|
|
269
|
-
out += "\n";
|
|
270
|
-
out += `export { revalidate } from "${RUNTIME_IMPORT_SPECIFIER}";\n`;
|
|
271
|
-
return out;
|
|
178
|
+
// Any non-v2 snapshot is corrupt as of Story 9.9. validateMetadata still
|
|
179
|
+
// produces a precise message for the common shape errors; otherwise reject
|
|
180
|
+
// the unsupported version explicitly.
|
|
181
|
+
validateMetadata(snapshot);
|
|
182
|
+
throw new Error(
|
|
183
|
+
`ruact server-function codegen: unsupported snapshot version ${JSON.stringify(snapshot.version)} ` +
|
|
184
|
+
`(only the route-driven version ${VERSION_V2} is supported as of Story 9.9); the bridge ` +
|
|
185
|
+
"JSON is corrupted — regenerate via `bin/rails ruact:server_functions:generate`.",
|
|
186
|
+
);
|
|
272
187
|
}
|
|
273
188
|
|
|
274
189
|
/**
|
|
@@ -368,6 +283,56 @@ function validateSnapshotV2(snapshot) {
|
|
|
368
283
|
"snapshot JSON is corrupted.",
|
|
369
284
|
);
|
|
370
285
|
}
|
|
286
|
+
// Story 13.4 — the structured query `params` metadata is a trust boundary
|
|
287
|
+
// (it becomes TS object keys). Mirrors Ruby `validate_params!`.
|
|
288
|
+
if (fn.kind === "query") validateQueryParams(fn);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateQueryParams(fn) {
|
|
293
|
+
// Story 13.4 review R1 — a non-Boolean truthy `params_rest` would otherwise
|
|
294
|
+
// silently flip a query to the open `& Record<string, unknown>` shape.
|
|
295
|
+
if (
|
|
296
|
+
fn.params_rest !== undefined &&
|
|
297
|
+
fn.params_rest !== null &&
|
|
298
|
+
typeof fn.params_rest !== "boolean"
|
|
299
|
+
) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`ruact server-function codegen: v2 query entry "${fn.js_identifier}" has a non-boolean ` +
|
|
302
|
+
`params_rest ${JSON.stringify(fn.params_rest)}; snapshot JSON is corrupted.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const params = fn.params;
|
|
307
|
+
if (params === undefined || params === null) return; // falls back to accepts_params
|
|
308
|
+
|
|
309
|
+
if (!Array.isArray(params)) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`ruact server-function codegen: v2 query entry "${fn.js_identifier}" has invalid ` +
|
|
312
|
+
`params ${JSON.stringify(params)} (must be an array of { name, required } objects).`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
for (const param of params) {
|
|
316
|
+
if (!param || typeof param !== "object" || Array.isArray(param)) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`ruact server-function codegen: v2 query entry "${fn.js_identifier}" has a params ` +
|
|
319
|
+
`element that is not an object: ${JSON.stringify(param)}; snapshot JSON is corrupted.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
if (typeof param.name !== "string" || param.name.length === 0 || containsLineTerminator(param.name)) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`ruact server-function codegen: v2 query entry "${fn.js_identifier}" has a params ` +
|
|
325
|
+
`name ${JSON.stringify(param.name)} that is not a non-empty single-line string; ` +
|
|
326
|
+
"snapshot JSON is corrupted.",
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
if (typeof param.required !== "boolean") {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`ruact server-function codegen: v2 query entry "${fn.js_identifier}" params name ` +
|
|
332
|
+
`${JSON.stringify(param.name)} has a non-boolean \`required\` ${JSON.stringify(param.required)}; ` +
|
|
333
|
+
"snapshot JSON is corrupted.",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
371
336
|
}
|
|
372
337
|
}
|
|
373
338
|
|
|
@@ -436,9 +401,7 @@ function renderExportV2(fn) {
|
|
|
436
401
|
// signature accepts params only when the query method declares kwargs (FR88).
|
|
437
402
|
// Mirrors `Ruact::ServerFunctions::Codegen::V2.render_query_export`.
|
|
438
403
|
function renderQueryExportV2(fn) {
|
|
439
|
-
const signature = fn
|
|
440
|
-
? "(params: Record<string, unknown>) => Promise<unknown>"
|
|
441
|
-
: "() => Promise<unknown>";
|
|
404
|
+
const signature = querySignatureV2(fn);
|
|
442
405
|
const pathLit = JSON.stringify(String(fn.path));
|
|
443
406
|
const descriptor = `{ path: ${pathLit}, kind: "query" }`;
|
|
444
407
|
return (
|
|
@@ -447,26 +410,38 @@ function renderQueryExportV2(fn) {
|
|
|
447
410
|
);
|
|
448
411
|
}
|
|
449
412
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
413
|
+
// Story 13.4 — picks the query accessor's `params` signature. With structured
|
|
414
|
+
// per-kwarg metadata (`fn.params`), build a typed object literal (required +
|
|
415
|
+
// optional exact, value type the FR88 wire union); empty params + no rest → the
|
|
416
|
+
// bare `()` signature; a `**keyrest` keeps the open `Record<string, unknown>`
|
|
417
|
+
// (intersected with any named keys). Falls back to the pre-13.4 `accepts_params`
|
|
418
|
+
// boolean when no `params` metadata is present. MUST mirror Ruby `query_signature`.
|
|
419
|
+
function querySignatureV2(fn) {
|
|
420
|
+
const params = fn.params;
|
|
421
|
+
if (!Array.isArray(params)) {
|
|
422
|
+
return fn.accepts_params
|
|
423
|
+
? "(params: Record<string, unknown>) => Promise<unknown>"
|
|
424
|
+
: "() => Promise<unknown>";
|
|
425
|
+
}
|
|
426
|
+
const rest = Boolean(fn.params_rest);
|
|
427
|
+
if (params.length === 0 && !rest) return "() => Promise<unknown>";
|
|
428
|
+
return buildParamsSignatureV2(params, rest);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function buildParamsSignatureV2(params, rest) {
|
|
432
|
+
const props = params.map(
|
|
433
|
+
(p) => `${formatParamKey(String(p.name))}${p.required ? "" : "?"}: ${QUERY_PARAM_VALUE_TYPE}`,
|
|
469
434
|
);
|
|
435
|
+
if (props.length === 0) return "(params: Record<string, unknown>) => Promise<unknown>"; // keyrest-only
|
|
436
|
+
let object = `{ ${props.join("; ")} }`;
|
|
437
|
+
if (rest) object += " & Record<string, unknown>";
|
|
438
|
+
return `(params: ${object}) => Promise<unknown>`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Quote any param key that is not a valid bare TS identifier (so a corrupted
|
|
442
|
+
// snapshot cannot break out of the object literal). Mirrors Ruby `format_param_key`.
|
|
443
|
+
function formatParamKey(name) {
|
|
444
|
+
return VALID_JS_IDENTIFIER.test(name) ? name : JSON.stringify(name);
|
|
470
445
|
}
|
|
471
446
|
|
|
472
447
|
/**
|