ruact 0.0.7 → 0.0.8
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/CHANGELOG.md +24 -0
- data/docs/internal/decisions/server-functions-api.md +55 -0
- data/lib/generators/ruact/install/install_generator.rb +114 -1
- data/lib/generators/ruact/install/templates/AGENTS.md.tt +159 -0
- data/lib/ruact/controller.rb +9 -0
- data/lib/ruact/doctor.rb +67 -9
- data/lib/ruact/erb_preprocessor.rb +113 -0
- data/lib/ruact/errors.rb +16 -0
- data/lib/ruact/manifest_resolver.rb +2 -2
- data/lib/ruact/serializable.rb +98 -6
- data/lib/ruact/server.rb +163 -0
- data/lib/ruact/server_functions/introspection.rb +81 -0
- data/lib/ruact/server_functions.rb +26 -4
- data/lib/ruact/testing/component_query.rb +113 -0
- data/lib/ruact/testing/flight_extractor.rb +221 -0
- data/lib/ruact/testing/flight_structure_diff.rb +267 -0
- data/lib/ruact/testing/flight_wire_parser.rb +138 -0
- data/lib/ruact/testing.rb +90 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/tasks/ruact.rake +55 -2
- data/spec/ruact/doctor_spec.rb +141 -0
- data/spec/ruact/erb_preprocessor_spec.rb +145 -0
- data/spec/ruact/install_generator_spec.rb +285 -0
- data/spec/ruact/manifest_resolver_spec.rb +15 -0
- data/spec/ruact/serializable_spec.rb +126 -0
- data/spec/ruact/server_bucket_request_spec.rb +291 -0
- data/spec/ruact/server_functions/introspection_spec.rb +135 -0
- data/spec/ruact/tasks_json_introspection_spec.rb +141 -0
- data/spec/ruact/testing/have_ruact_component_spec.rb +170 -0
- data/spec/ruact/testing/no_production_load_spec.rb +41 -0
- data/spec/support/flight_wire_parser.rb +12 -126
- data/spec/support/matchers/flight_fixture_matcher.rb +7 -258
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +25 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +104 -7
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +173 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts +25 -0
- metadata +14 -2
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
// enforces). Reads are CSRF-free (GET semantics).
|
|
15
15
|
//
|
|
16
16
|
// The export surface (`_makeServerFunction`, `_makeQuery`, `useQuery`,
|
|
17
|
-
// `revalidate`, `configureRuactRuntime`, `RuactActionError`) and
|
|
18
|
-
// `"ruact/server-functions-runtime"` import path are part of the locked API —
|
|
19
|
-
// do NOT change without coordinated codegen + Vite-plugin updates.
|
|
17
|
+
// `revalidate`, `withRefresh`, `configureRuactRuntime`, `RuactActionError`) and
|
|
18
|
+
// the `"ruact/server-functions-runtime"` import path are part of the locked API —
|
|
19
|
+
// do NOT change without coordinated codegen + Vite-plugin updates. (Story 15.6
|
|
20
|
+
// added `withRefresh` — a runtime-only wrapper; it is NOT re-exported through the
|
|
21
|
+
// generated `.ruact/server-functions.ts` barrel, so codegen output is unchanged.)
|
|
20
22
|
|
|
21
23
|
// Story 9.5 — `useQuery` is a React hook, so the runtime now depends on React.
|
|
22
24
|
// React is declared as a `peerDependency` (every host already has it; the
|
|
@@ -33,8 +35,16 @@ const RUNTIME_VERSION = 1;
|
|
|
33
35
|
// them; instead, hosts call `configureRuactRuntime` once at app boot
|
|
34
36
|
// to register a headers-producing function. The function runs on
|
|
35
37
|
// every fetch so dynamic tokens (refreshed at runtime) are picked up.
|
|
38
|
+
// Story 15.6 (FR110) — `autoRevalidate` is the app-wide default for the
|
|
39
|
+
// auto-revalidate opt-in: when `true`, every successful, NON-redirecting mutation
|
|
40
|
+
// (`_makeServerFunction` accessor) `await`s an in-place Flight refresh of the
|
|
41
|
+
// current path (via the existing `revalidate()`) BEFORE resolving with the
|
|
42
|
+
// action's JSON result. Default `false` → today's behavior, byte-identical. A
|
|
43
|
+
// per-call `withRefresh(accessor)` wrapper forces the refresh for a single call
|
|
44
|
+
// and WINS over this global (D5: per-call explicit beats the global default).
|
|
36
45
|
const runtimeOptions = {
|
|
37
46
|
defaultHeaders: null,
|
|
47
|
+
autoRevalidate: false,
|
|
38
48
|
};
|
|
39
49
|
export function configureRuactRuntime(options) {
|
|
40
50
|
if (options && Object.prototype.hasOwnProperty.call(options, "defaultHeaders")) {
|
|
@@ -47,6 +57,15 @@ export function configureRuactRuntime(options) {
|
|
|
47
57
|
);
|
|
48
58
|
}
|
|
49
59
|
}
|
|
60
|
+
if (options && Object.prototype.hasOwnProperty.call(options, "autoRevalidate")) {
|
|
61
|
+
const value = options.autoRevalidate;
|
|
62
|
+
if (typeof value !== "boolean") {
|
|
63
|
+
throw new TypeError(
|
|
64
|
+
"configureRuactRuntime: autoRevalidate must be a boolean",
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
runtimeOptions.autoRevalidate = value;
|
|
68
|
+
}
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
@@ -101,12 +120,28 @@ export class RuactActionError extends Error {
|
|
|
101
120
|
* (`globalThis.__ruact_navigate`, `window.location.assign` fallback) and
|
|
102
121
|
* resolves `null`.
|
|
103
122
|
*
|
|
123
|
+
* Story 15.6 (FR110) — auto-revalidate opt-in. When the call opts in — via the
|
|
124
|
+
* app-wide `configureRuactRuntime({ autoRevalidate: true })` default OR the
|
|
125
|
+
* per-call `withRefresh(accessor)` wrapper — a SUCCESSFUL, NON-redirecting
|
|
126
|
+
* mutation `await`s an in-place Flight refresh of the current path (the existing
|
|
127
|
+
* `revalidate()`) BEFORE the promise resolves, then resolves with the action's
|
|
128
|
+
* JSON result. This is pure client-side COMPOSITION of the two requests that
|
|
129
|
+
* already exist (POST-JSON mutation + GET-Flight refresh) — no new wire shape, no
|
|
130
|
+
* inbound Flight deserialization (serialize-only invariant intact). A `$redirect`
|
|
131
|
+
* WINS: the redirect is followed and the refresh is SKIPPED (refreshing the old
|
|
132
|
+
* path would be wrong). A failed mutation throws before the refresh runs, so no
|
|
133
|
+
* refresh happens. If the refresh is requested with no router installed, the
|
|
134
|
+
* descriptive `revalidate()` error surfaces (it is not swallowed).
|
|
135
|
+
*
|
|
104
136
|
* @param {{ method: string, path: string, segments?: string[] }} descriptor
|
|
105
137
|
* @returns {(arg1?: Record<string, unknown> | FormData, arg2?: FormData | Record<string, unknown>) => Promise<unknown>}
|
|
106
138
|
*/
|
|
107
139
|
export function _makeServerFunction(descriptor) {
|
|
108
140
|
const { method, path, segments = [] } = descriptor || {};
|
|
109
|
-
|
|
141
|
+
// The core invoker takes the raw call-args plus a `refreshOverride`:
|
|
142
|
+
// undefined → fall back to the app-wide `runtimeOptions.autoRevalidate`;
|
|
143
|
+
// true/false → an explicit per-call decision that WINS over the global (D5).
|
|
144
|
+
const invoke = async function (callArgs, refreshOverride) {
|
|
110
145
|
if (callArgs.length > 2) {
|
|
111
146
|
throw new TypeError(
|
|
112
147
|
`ruact server function ${method} ${path} called with ${callArgs.length} arguments — ` +
|
|
@@ -115,8 +150,61 @@ export function _makeServerFunction(descriptor) {
|
|
|
115
150
|
}
|
|
116
151
|
const args = pickWirePayload(callArgs);
|
|
117
152
|
const url = interpolatePath(path, segments, args);
|
|
153
|
+
// AC4 — a failed mutation throws HERE, before any refresh is sequenced.
|
|
118
154
|
const parsed = await ruactInvoke({ method, url, args, label: path });
|
|
119
|
-
|
|
155
|
+
const { value, redirected } = followRedirect(parsed);
|
|
156
|
+
// AC2 — redirect wins: the destination already re-renders; skip the refresh.
|
|
157
|
+
if (redirected) return value;
|
|
158
|
+
const refresh =
|
|
159
|
+
refreshOverride === undefined ? runtimeOptions.autoRevalidate === true : refreshOverride === true;
|
|
160
|
+
if (refresh) {
|
|
161
|
+
// AC1 — refresh the current path in place BEFORE resolving. AC5 — if no
|
|
162
|
+
// router is installed, `revalidate()`'s descriptive error surfaces here
|
|
163
|
+
// (D3: a mutation that succeeded but whose refresh rejects rejects the
|
|
164
|
+
// promise — consistent with `revalidate()`'s loud-by-default stance).
|
|
165
|
+
await revalidate();
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
};
|
|
169
|
+
const accessor = function ruactServerFunctionCall(...callArgs) {
|
|
170
|
+
return invoke(callArgs, undefined);
|
|
171
|
+
};
|
|
172
|
+
// Stash the core invoker on the accessor (non-enumerable) so `withRefresh` can
|
|
173
|
+
// force a refresh for a single call WITHOUT the codegen emitting a different
|
|
174
|
+
// accessor shape — the generated `.ruact/server-functions.ts` stays byte-
|
|
175
|
+
// identical (AC6). Keyed by a module-private Symbol so host code can't collide.
|
|
176
|
+
Object.defineProperty(accessor, REFRESH_INVOKER, { value: invoke, enumerable: false });
|
|
177
|
+
return accessor;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Story 15.6 (FR110) — module-private handle used by `withRefresh` to reach a
|
|
181
|
+
// server-function accessor's core invoker (see `_makeServerFunction`).
|
|
182
|
+
const REFRESH_INVOKER = Symbol("ruactRefreshInvoker");
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Story 15.6 (FR110) — per-call auto-revalidate wrapper. Wraps a generated
|
|
186
|
+
* server-function accessor and returns a NEW accessor that forces the in-place
|
|
187
|
+
* Flight refresh after a successful, non-redirecting call — regardless of the
|
|
188
|
+
* app-wide `autoRevalidate` default (D5: per-call explicit wins). Runtime-only:
|
|
189
|
+
* it does not change the codegen or the wire.
|
|
190
|
+
*
|
|
191
|
+
* import { createPost } from "@/.ruact/server-functions";
|
|
192
|
+
* import { withRefresh } from "ruact/server-functions-runtime";
|
|
193
|
+
* await withRefresh(createPost)(formData); // mutation + refresh in one await
|
|
194
|
+
*
|
|
195
|
+
* @param {Function} accessor A `_makeServerFunction`-produced accessor.
|
|
196
|
+
* @returns {(...args: unknown[]) => Promise<unknown>} A refreshing accessor.
|
|
197
|
+
*/
|
|
198
|
+
export function withRefresh(accessor) {
|
|
199
|
+
const invoke = typeof accessor === "function" ? accessor[REFRESH_INVOKER] : undefined;
|
|
200
|
+
if (typeof invoke !== "function") {
|
|
201
|
+
throw new TypeError(
|
|
202
|
+
"ruact withRefresh() expects a server-function accessor produced by the ruact codegen " +
|
|
203
|
+
"(imported from @/.ruact/server-functions)",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return function ruactRefreshingCall(...callArgs) {
|
|
207
|
+
return invoke(callArgs, true);
|
|
120
208
|
};
|
|
121
209
|
}
|
|
122
210
|
|
|
@@ -519,6 +607,15 @@ function readSegment(args, seg) {
|
|
|
519
607
|
// `{ "$redirect": "<path>" }`; follow it via the router handoff and resolve
|
|
520
608
|
// `null` (consistent with the 204→null contract).
|
|
521
609
|
function followRedirectIfPresent(parsed) {
|
|
610
|
+
return followRedirect(parsed).value;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Story 15.6 — the same follow logic, but reporting WHETHER a redirect was
|
|
614
|
+
// followed as an explicit boolean. Auto-revalidate needs to distinguish "a
|
|
615
|
+
// redirect was followed → skip the refresh" from "the action genuinely resolved
|
|
616
|
+
// `null`/204 → still refresh" — inferring redirect from `value === null` would
|
|
617
|
+
// wrongly skip the refresh after an ordinary empty-body mutation.
|
|
618
|
+
function followRedirect(parsed) {
|
|
522
619
|
if (
|
|
523
620
|
parsed &&
|
|
524
621
|
typeof parsed === "object" &&
|
|
@@ -526,9 +623,9 @@ function followRedirectIfPresent(parsed) {
|
|
|
526
623
|
typeof parsed.$redirect === "string"
|
|
527
624
|
) {
|
|
528
625
|
navigateTo(parsed.$redirect);
|
|
529
|
-
return null;
|
|
626
|
+
return { value: null, redirected: true };
|
|
530
627
|
}
|
|
531
|
-
return parsed;
|
|
628
|
+
return { value: parsed, redirected: false };
|
|
532
629
|
}
|
|
533
630
|
|
|
534
631
|
function navigateTo(target) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
RuactActionError,
|
|
16
16
|
configureRuactRuntime,
|
|
17
17
|
revalidate,
|
|
18
|
+
withRefresh,
|
|
18
19
|
} from "./index.js";
|
|
19
20
|
|
|
20
21
|
// Helper — a route-driven accessor targeting `POST /posts` (the canonical
|
|
@@ -822,3 +823,175 @@ describe("Story 9.3 — _makeServerFunction (route-driven, real path+verb)", ()
|
|
|
822
823
|
expect(result).toEqual({ post: { id: 1 }, $redirect: 42 });
|
|
823
824
|
});
|
|
824
825
|
});
|
|
826
|
+
|
|
827
|
+
// =============================================================================
|
|
828
|
+
// Story 15.6 (FR110) — auto-revalidate opt-in: one await = mutation + refresh
|
|
829
|
+
// =============================================================================
|
|
830
|
+
|
|
831
|
+
describe("Story 15.6 — auto-revalidate opt-in", () => {
|
|
832
|
+
let originalRevalidate;
|
|
833
|
+
let originalNavigate;
|
|
834
|
+
let originalLocation;
|
|
835
|
+
let originalWindow;
|
|
836
|
+
|
|
837
|
+
beforeEach(() => {
|
|
838
|
+
originalRevalidate = globalThis.__ruact_revalidate;
|
|
839
|
+
originalNavigate = globalThis.__ruact_navigate;
|
|
840
|
+
originalLocation = globalThis.location;
|
|
841
|
+
originalWindow = globalThis.window;
|
|
842
|
+
globalThis.location = { pathname: "/posts", search: "?page=2" };
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
afterEach(() => {
|
|
846
|
+
// Reset the app-wide default so a leaked `autoRevalidate: true` can't bleed
|
|
847
|
+
// into the un-opted-in byte-parity tests elsewhere in the suite.
|
|
848
|
+
configureRuactRuntime({ autoRevalidate: false });
|
|
849
|
+
if (originalRevalidate === undefined) delete globalThis.__ruact_revalidate;
|
|
850
|
+
else globalThis.__ruact_revalidate = originalRevalidate;
|
|
851
|
+
globalThis.__ruact_navigate = originalNavigate;
|
|
852
|
+
if (originalLocation === undefined) {
|
|
853
|
+
// jsdom / browser env — leave the real `location` alone
|
|
854
|
+
} else {
|
|
855
|
+
globalThis.location = originalLocation;
|
|
856
|
+
}
|
|
857
|
+
globalThis.window = originalWindow;
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// (a) request-composition order — POST then Flight refresh, in that order,
|
|
861
|
+
// resolves with the action's JSON result.
|
|
862
|
+
it("global default: refreshes the current path AFTER the mutation and resolves the JSON result", async () => {
|
|
863
|
+
const order = [];
|
|
864
|
+
mockFetchOk({ id: 7 });
|
|
865
|
+
const fetchSpy = globalThis.fetch;
|
|
866
|
+
fetchSpy.mockImplementation(async () => {
|
|
867
|
+
order.push("fetch");
|
|
868
|
+
return {
|
|
869
|
+
ok: true,
|
|
870
|
+
status: 200,
|
|
871
|
+
headers: { get: (n) => (n.toLowerCase() === "content-type" ? "application/json" : null) },
|
|
872
|
+
text: vi.fn().mockResolvedValue(JSON.stringify({ id: 7 })),
|
|
873
|
+
json: vi.fn().mockResolvedValue({ id: 7 }),
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
const revalidateSpy = vi.fn(async () => {
|
|
877
|
+
order.push("revalidate");
|
|
878
|
+
});
|
|
879
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
880
|
+
configureRuactRuntime({ autoRevalidate: true });
|
|
881
|
+
|
|
882
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
883
|
+
const result = await createPost({ title: "x" });
|
|
884
|
+
|
|
885
|
+
expect(order).toEqual(["fetch", "revalidate"]);
|
|
886
|
+
expect(revalidateSpy).toHaveBeenCalledWith("/posts?page=2");
|
|
887
|
+
expect(result).toEqual({ id: 7 });
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it("per-call withRefresh: forces the refresh even when the global default is OFF", async () => {
|
|
891
|
+
mockFetchOk({ id: 1 });
|
|
892
|
+
const revalidateSpy = vi.fn().mockResolvedValue(undefined);
|
|
893
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
894
|
+
configureRuactRuntime({ autoRevalidate: false });
|
|
895
|
+
|
|
896
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
897
|
+
const result = await withRefresh(createPost)({ title: "x" });
|
|
898
|
+
|
|
899
|
+
expect(revalidateSpy).toHaveBeenCalledWith("/posts?page=2");
|
|
900
|
+
expect(result).toEqual({ id: 1 });
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("refreshes after a genuine null/204 result (not misread as a redirect)", async () => {
|
|
904
|
+
mockFetchOk("", { status: 204, contentType: "text/plain" });
|
|
905
|
+
const revalidateSpy = vi.fn().mockResolvedValue(undefined);
|
|
906
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
907
|
+
|
|
908
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
909
|
+
const result = await withRefresh(createPost)({});
|
|
910
|
+
|
|
911
|
+
expect(revalidateSpy).toHaveBeenCalledTimes(1);
|
|
912
|
+
expect(result).toBeNull();
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// (b) error propagation — a failed mutation rejects and issues NO refresh.
|
|
916
|
+
it("mutation failure propagates and does NOT refresh (refresh sequenced after success)", async () => {
|
|
917
|
+
mockFetchError(422, JSON.stringify({ error: "invalid" }));
|
|
918
|
+
const revalidateSpy = vi.fn().mockResolvedValue(undefined);
|
|
919
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
920
|
+
configureRuactRuntime({ autoRevalidate: true });
|
|
921
|
+
|
|
922
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
923
|
+
|
|
924
|
+
await expect(createPost({ title: "" })).rejects.toMatchObject({
|
|
925
|
+
name: "RuactActionError",
|
|
926
|
+
status: 422,
|
|
927
|
+
});
|
|
928
|
+
expect(revalidateSpy).not.toHaveBeenCalled();
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
// (c) redirect-wins — a $redirect follows the redirect and issues NO refresh.
|
|
932
|
+
it("redirect wins: follows the $redirect, resolves null, and SKIPS the refresh", async () => {
|
|
933
|
+
mockFetchOk({ $redirect: "/posts/1" });
|
|
934
|
+
const navSpy = vi.fn();
|
|
935
|
+
globalThis.__ruact_navigate = navSpy;
|
|
936
|
+
const revalidateSpy = vi.fn().mockResolvedValue(undefined);
|
|
937
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
938
|
+
configureRuactRuntime({ autoRevalidate: true });
|
|
939
|
+
|
|
940
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
941
|
+
const result = await createPost({ title: "x" });
|
|
942
|
+
|
|
943
|
+
expect(navSpy).toHaveBeenCalledWith("/posts/1");
|
|
944
|
+
expect(result).toBeNull();
|
|
945
|
+
expect(revalidateSpy).not.toHaveBeenCalled();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// (d) no-router — the descriptive revalidate() error surfaces (not swallowed).
|
|
949
|
+
it("no router installed: surfaces the descriptive revalidate() error", async () => {
|
|
950
|
+
mockFetchOk({ id: 1 });
|
|
951
|
+
if ("__ruact_revalidate" in globalThis) delete globalThis.__ruact_revalidate;
|
|
952
|
+
configureRuactRuntime({ autoRevalidate: true });
|
|
953
|
+
|
|
954
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
955
|
+
|
|
956
|
+
await expect(createPost({ title: "x" })).rejects.toThrow(
|
|
957
|
+
/ruact: revalidate\(\) called but no router is installed/,
|
|
958
|
+
);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
it("D3: a successful mutation whose refresh REJECTS rejects the returned promise", async () => {
|
|
962
|
+
mockFetchOk({ id: 9 });
|
|
963
|
+
const revalidateSpy = vi.fn().mockRejectedValue(new Error("Flight refresh failed"));
|
|
964
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
965
|
+
|
|
966
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
967
|
+
|
|
968
|
+
await expect(withRefresh(createPost)({ title: "x" })).rejects.toThrow("Flight refresh failed");
|
|
969
|
+
expect(revalidateSpy).toHaveBeenCalledTimes(1);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// (e) un-opted-in is byte-identical to today: exactly one fetch, no refresh.
|
|
973
|
+
it("un-opted-in call is byte-identical: one fetch, no refresh", async () => {
|
|
974
|
+
mockFetchOk({ id: 1 });
|
|
975
|
+
const revalidateSpy = vi.fn().mockResolvedValue(undefined);
|
|
976
|
+
globalThis.__ruact_revalidate = revalidateSpy;
|
|
977
|
+
// global default stays OFF (reset in afterEach), no withRefresh wrapper.
|
|
978
|
+
|
|
979
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
980
|
+
const result = await createPost({ title: "x" });
|
|
981
|
+
|
|
982
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
983
|
+
expect(revalidateSpy).not.toHaveBeenCalled();
|
|
984
|
+
expect(result).toEqual({ id: 1 });
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it("withRefresh throws a descriptive TypeError for a non-accessor argument", () => {
|
|
988
|
+
expect(() => withRefresh(() => {})).toThrow(/server-function accessor produced by the ruact codegen/);
|
|
989
|
+
expect(() => withRefresh(null)).toThrow(TypeError);
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
it("configureRuactRuntime rejects a non-boolean autoRevalidate", () => {
|
|
993
|
+
expect(() => configureRuactRuntime({ autoRevalidate: "yes" })).toThrow(
|
|
994
|
+
/autoRevalidate must be a boolean/,
|
|
995
|
+
);
|
|
996
|
+
});
|
|
997
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Story 15.6 (FR110) — type-level test for the auto-revalidate opt-in surface.
|
|
2
|
+
// Proves (a) `configureRuactRuntime` accepts the new `autoRevalidate?: boolean`
|
|
3
|
+
// key alongside `defaultHeaders`, and (b) `withRefresh` preserves the wrapped
|
|
4
|
+
// accessor's call signature (so `withRefresh(createPost)` is callable exactly
|
|
5
|
+
// like `createPost`). Runtime-only: this surface is NOT emitted by the codegen,
|
|
6
|
+
// so there is no byte-parity fixture to regenerate.
|
|
7
|
+
|
|
8
|
+
import { _makeServerFunction, configureRuactRuntime, withRefresh } from "ruact/server-functions-runtime";
|
|
9
|
+
|
|
10
|
+
// (a) the app-wide default key type-checks (and stays optional).
|
|
11
|
+
configureRuactRuntime({ autoRevalidate: true });
|
|
12
|
+
configureRuactRuntime({ autoRevalidate: false, defaultHeaders: { Authorization: "Bearer x" } });
|
|
13
|
+
configureRuactRuntime({ defaultHeaders: null });
|
|
14
|
+
|
|
15
|
+
// A non-boolean autoRevalidate is a type error.
|
|
16
|
+
// @ts-expect-error autoRevalidate must be a boolean
|
|
17
|
+
configureRuactRuntime({ autoRevalidate: "yes" });
|
|
18
|
+
|
|
19
|
+
// (b) withRefresh preserves the accessor call signature.
|
|
20
|
+
const createPost = _makeServerFunction({ method: "POST", path: "/posts", segments: [] });
|
|
21
|
+
const createPostRefreshing = withRefresh(createPost);
|
|
22
|
+
|
|
23
|
+
// Callable at the same shapes as the underlying accessor.
|
|
24
|
+
void createPostRefreshing({ title: "x" });
|
|
25
|
+
void createPostRefreshing();
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruact
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Luiz Garcia
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: nokogiri
|
|
@@ -46,6 +46,7 @@ files:
|
|
|
46
46
|
- docs/internal/README.md
|
|
47
47
|
- docs/internal/decisions/server-functions-api.md
|
|
48
48
|
- lib/generators/ruact/install/install_generator.rb
|
|
49
|
+
- lib/generators/ruact/install/templates/AGENTS.md.tt
|
|
49
50
|
- lib/generators/ruact/install/templates/Procfile.dev.tt
|
|
50
51
|
- lib/generators/ruact/install/templates/dev.tt
|
|
51
52
|
- lib/generators/ruact/install/templates/initializer.rb.tt
|
|
@@ -102,6 +103,7 @@ files:
|
|
|
102
103
|
- lib/ruact/server_functions/error_payload.rb
|
|
103
104
|
- lib/ruact/server_functions/error_rendering.rb
|
|
104
105
|
- lib/ruact/server_functions/error_suggestion.rb
|
|
106
|
+
- lib/ruact/server_functions/introspection.rb
|
|
105
107
|
- lib/ruact/server_functions/name_bridge.rb
|
|
106
108
|
- lib/ruact/server_functions/query_context.rb
|
|
107
109
|
- lib/ruact/server_functions/query_dispatch.rb
|
|
@@ -112,6 +114,11 @@ files:
|
|
|
112
114
|
- lib/ruact/server_functions/validation_errors.rb
|
|
113
115
|
- lib/ruact/signed_references.rb
|
|
114
116
|
- lib/ruact/string_distance.rb
|
|
117
|
+
- lib/ruact/testing.rb
|
|
118
|
+
- lib/ruact/testing/component_query.rb
|
|
119
|
+
- lib/ruact/testing/flight_extractor.rb
|
|
120
|
+
- lib/ruact/testing/flight_structure_diff.rb
|
|
121
|
+
- lib/ruact/testing/flight_wire_parser.rb
|
|
115
122
|
- lib/ruact/validation_errors_collector.rb
|
|
116
123
|
- lib/ruact/version.rb
|
|
117
124
|
- lib/ruact/view_helper.rb
|
|
@@ -176,6 +183,7 @@ files:
|
|
|
176
183
|
- spec/ruact/server_functions/codegen_spec.rb
|
|
177
184
|
- spec/ruact/server_functions/error_payload_spec.rb
|
|
178
185
|
- spec/ruact/server_functions/error_suggestion_spec.rb
|
|
186
|
+
- spec/ruact/server_functions/introspection_spec.rb
|
|
179
187
|
- spec/ruact/server_functions/name_bridge_spec.rb
|
|
180
188
|
- spec/ruact/server_functions/query_context_spec.rb
|
|
181
189
|
- spec/ruact/server_functions/query_source_spec.rb
|
|
@@ -189,6 +197,9 @@ files:
|
|
|
189
197
|
- spec/ruact/server_upload_request_spec.rb
|
|
190
198
|
- spec/ruact/signed_references_spec.rb
|
|
191
199
|
- spec/ruact/string_distance_spec.rb
|
|
200
|
+
- spec/ruact/tasks_json_introspection_spec.rb
|
|
201
|
+
- spec/ruact/testing/have_ruact_component_spec.rb
|
|
202
|
+
- spec/ruact/testing/no_production_load_spec.rb
|
|
192
203
|
- spec/ruact/validation_errors_spec.rb
|
|
193
204
|
- spec/ruact/view_helper_spec.rb
|
|
194
205
|
- spec/spec_helper.rb
|
|
@@ -218,6 +229,7 @@ files:
|
|
|
218
229
|
- vendor/javascript/vite-plugin-ruact/tsconfig.json
|
|
219
230
|
- vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json
|
|
220
231
|
- vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json
|
|
232
|
+
- vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts
|
|
221
233
|
- vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts
|
|
222
234
|
- vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx
|
|
223
235
|
- vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx
|