ruact 0.0.6 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/docs/internal/decisions/server-functions-api.md +55 -0
  4. data/lib/generators/ruact/install/install_generator.rb +114 -1
  5. data/lib/generators/ruact/install/templates/AGENTS.md.tt +159 -0
  6. data/lib/ruact/controller.rb +18 -3
  7. data/lib/ruact/doctor.rb +67 -9
  8. data/lib/ruact/erb_preprocessor.rb +120 -1
  9. data/lib/ruact/errors.rb +16 -0
  10. data/lib/ruact/manifest_resolver.rb +149 -0
  11. data/lib/ruact/railtie.rb +14 -3
  12. data/lib/ruact/render_pipeline.rb +8 -1
  13. data/lib/ruact/serializable.rb +98 -6
  14. data/lib/ruact/server.rb +163 -0
  15. data/lib/ruact/server_functions/introspection.rb +81 -0
  16. data/lib/ruact/server_functions.rb +26 -4
  17. data/lib/ruact/testing/component_query.rb +113 -0
  18. data/lib/ruact/testing/flight_extractor.rb +221 -0
  19. data/lib/ruact/testing/flight_structure_diff.rb +267 -0
  20. data/lib/ruact/testing/flight_wire_parser.rb +138 -0
  21. data/lib/ruact/testing.rb +90 -0
  22. data/lib/ruact/version.rb +1 -1
  23. data/lib/ruact/view_helper.rb +11 -4
  24. data/lib/ruact.rb +1 -0
  25. data/lib/tasks/ruact.rake +55 -2
  26. data/spec/ruact/controller_spec.rb +7 -2
  27. data/spec/ruact/doctor_spec.rb +141 -0
  28. data/spec/ruact/erb_preprocessor_spec.rb +145 -0
  29. data/spec/ruact/install_generator_spec.rb +285 -0
  30. data/spec/ruact/manifest_resolver_spec.rb +174 -0
  31. data/spec/ruact/serializable_spec.rb +126 -0
  32. data/spec/ruact/server_bucket_request_spec.rb +291 -0
  33. data/spec/ruact/server_functions/introspection_spec.rb +135 -0
  34. data/spec/ruact/tasks_json_introspection_spec.rb +141 -0
  35. data/spec/ruact/testing/have_ruact_component_spec.rb +170 -0
  36. data/spec/ruact/testing/no_production_load_spec.rb +41 -0
  37. data/spec/spec_helper.rb +15 -0
  38. data/spec/support/flight_wire_parser.rb +12 -126
  39. data/spec/support/matchers/flight_fixture_matcher.rb +7 -258
  40. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +25 -0
  41. data/vendor/javascript/ruact-server-functions-runtime/index.js +104 -7
  42. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +173 -0
  43. data/vendor/javascript/vite-plugin-ruact/index.js +20 -0
  44. data/vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs +125 -0
  45. data/vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts +25 -0
  46. metadata +17 -2
@@ -171,6 +171,26 @@ export default function ruact(options = {}) {
171
171
  configureServer(server) {
172
172
  const dir = path.resolve(root, componentsDir);
173
173
  server.watcher.add(dir);
174
+
175
+ // Serve the LIVE in-memory manifest over HTTP so the Rails gem can
176
+ // resolve components in dev without depending on the on-disk
177
+ // public/react-client-manifest.json — whose first write races Rails's
178
+ // boot-time read (Rails can boot, and config.to_prepare read the file,
179
+ // BEFORE Vite writes it → Ruact.manifest nil → 500 on the first request).
180
+ // This endpoint always reflects the current `manifest` (rebuilt by the
181
+ // watcher below + buildStart), and omits the internal `_sourceFile` field
182
+ // so the wire shape matches the file the gem already knows how to parse.
183
+ server.middlewares.use("/__ruact/manifest", (req, res, next) => {
184
+ if (req.method !== "GET") return next();
185
+ const out = {};
186
+ for (const [name, entry] of Object.entries(manifest)) {
187
+ const { _sourceFile, ...rest } = entry;
188
+ out[name] = rest;
189
+ }
190
+ res.setHeader("Content-Type", "application/json");
191
+ res.end(JSON.stringify(out));
192
+ });
193
+
174
194
  const rebuild = (file) => {
175
195
  if (!file.startsWith(dir)) return;
176
196
  manifest = buildManifest(dir);
@@ -0,0 +1,125 @@
1
+ // Fix (manifest-over-HTTP) — the dev server exposes the live in-memory manifest
2
+ // at GET /__ruact/manifest so the Rails gem can resolve components without
3
+ // racing the on-disk react-client-manifest.json write at boot. This file
4
+ // covers the configureServer middleware:
5
+ //
6
+ // 1. responds with the current manifest (no internal `_sourceFile` field);
7
+ // 2. reflects a rebuild when a "use client" component is added/changed.
8
+
9
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import ruact from "./index.js";
14
+
15
+ let dir;
16
+ beforeEach(() => {
17
+ dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "ruact-manifest-http-")));
18
+ });
19
+ afterEach(() => {
20
+ fs.rmSync(dir, { recursive: true, force: true });
21
+ });
22
+
23
+ function write(rel, body) {
24
+ const full = path.join(dir, rel);
25
+ fs.mkdirSync(path.dirname(full), { recursive: true });
26
+ fs.writeFileSync(full, body);
27
+ return full;
28
+ }
29
+
30
+ const COMPONENT = (name) =>
31
+ `"use client";\nexport function ${name}() { return null; }\n`;
32
+
33
+ // A minimal connect-style server harness: captures the path-mounted middleware
34
+ // and the watcher callbacks the plugin registers in configureServer.
35
+ function makeServer() {
36
+ const middlewares = new Map();
37
+ const watcherHandlers = { change: [], add: [], unlink: [] };
38
+ return {
39
+ middlewares: { use: (route, fn) => middlewares.set(route, fn) },
40
+ moduleGraph: { getModuleById: () => null, invalidateModule: () => {} },
41
+ ws: { send: () => {} },
42
+ watcher: {
43
+ add: () => {},
44
+ on: (event, fn) => {
45
+ (watcherHandlers[event] ||= []).push(fn);
46
+ },
47
+ },
48
+ _invokeMiddleware(route, method = "GET") {
49
+ const fn = middlewares.get(route);
50
+ let body = "";
51
+ let nextCalled = false;
52
+ const headers = {};
53
+ const res = {
54
+ setHeader: (k, v) => {
55
+ headers[k] = v;
56
+ },
57
+ end: (chunk) => {
58
+ body = chunk;
59
+ },
60
+ };
61
+ fn({ url: route, method }, res, () => {
62
+ nextCalled = true;
63
+ });
64
+ return { body, headers, nextCalled };
65
+ },
66
+ _fireChange(file) {
67
+ for (const fn of watcherHandlers.change) fn(file);
68
+ },
69
+ };
70
+ }
71
+
72
+ async function bootPlugin() {
73
+ const plugin = ruact();
74
+ await plugin.configResolved({ root: dir });
75
+ await plugin.buildStart({});
76
+ const server = makeServer();
77
+ await plugin.configureServer(server);
78
+ return server;
79
+ }
80
+
81
+ describe("GET /__ruact/manifest (dev server middleware)", () => {
82
+ it("serves the current manifest as JSON without the internal _sourceFile field", async () => {
83
+ write("app/javascript/components/LikeButton.jsx", COMPONENT("LikeButton"));
84
+ const server = await bootPlugin();
85
+
86
+ const { body, headers } = server._invokeMiddleware("/__ruact/manifest");
87
+ expect(headers["Content-Type"]).toBe("application/json");
88
+
89
+ const manifest = JSON.parse(body);
90
+ expect(manifest).toHaveProperty("LikeButton");
91
+ expect(manifest.LikeButton).toMatchObject({
92
+ id: "/LikeButton.jsx",
93
+ name: "LikeButton",
94
+ chunks: ["/LikeButton.jsx"],
95
+ });
96
+ // The internal field used only during the prod build must never go on the wire.
97
+ expect(manifest.LikeButton).not.toHaveProperty("_sourceFile");
98
+ });
99
+
100
+ it("reflects a rebuild when a new component is added", async () => {
101
+ write("app/javascript/components/LikeButton.jsx", COMPONENT("LikeButton"));
102
+ const server = await bootPlugin();
103
+
104
+ expect(JSON.parse(server._invokeMiddleware("/__ruact/manifest").body)).not.toHaveProperty(
105
+ "CommentBox"
106
+ );
107
+
108
+ const added = write("app/javascript/components/CommentBox.jsx", COMPONENT("CommentBox"));
109
+ server._fireChange(added);
110
+
111
+ const manifest = JSON.parse(server._invokeMiddleware("/__ruact/manifest").body);
112
+ expect(manifest).toHaveProperty("LikeButton");
113
+ expect(manifest).toHaveProperty("CommentBox");
114
+ expect(manifest.CommentBox).not.toHaveProperty("_sourceFile");
115
+ });
116
+
117
+ it("falls through (calls next) for non-GET methods", async () => {
118
+ write("app/javascript/components/LikeButton.jsx", COMPONENT("LikeButton"));
119
+ const server = await bootPlugin();
120
+
121
+ const { body, nextCalled } = server._invokeMiddleware("/__ruact/manifest", "POST");
122
+ expect(nextCalled).toBe(true);
123
+ expect(body).toBe("");
124
+ });
125
+ });
@@ -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.6
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-06-30 00:00:00.000000000 Z
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
@@ -85,6 +86,7 @@ files:
85
86
  - lib/ruact/flight/row_emitter.rb
86
87
  - lib/ruact/flight/serializer.rb
87
88
  - lib/ruact/html_converter.rb
89
+ - lib/ruact/manifest_resolver.rb
88
90
  - lib/ruact/query.rb
89
91
  - lib/ruact/railtie.rb
90
92
  - lib/ruact/render_context.rb
@@ -101,6 +103,7 @@ files:
101
103
  - lib/ruact/server_functions/error_payload.rb
102
104
  - lib/ruact/server_functions/error_rendering.rb
103
105
  - lib/ruact/server_functions/error_suggestion.rb
106
+ - lib/ruact/server_functions/introspection.rb
104
107
  - lib/ruact/server_functions/name_bridge.rb
105
108
  - lib/ruact/server_functions/query_context.rb
106
109
  - lib/ruact/server_functions/query_dispatch.rb
@@ -111,6 +114,11 @@ files:
111
114
  - lib/ruact/server_functions/validation_errors.rb
112
115
  - lib/ruact/signed_references.rb
113
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
114
122
  - lib/ruact/validation_errors_collector.rb
115
123
  - lib/ruact/version.rb
116
124
  - lib/ruact/view_helper.rb
@@ -159,6 +167,7 @@ files:
159
167
  - spec/ruact/flight/serializer_spec.rb
160
168
  - spec/ruact/html_converter_spec.rb
161
169
  - spec/ruact/install_generator_spec.rb
170
+ - spec/ruact/manifest_resolver_spec.rb
162
171
  - spec/ruact/query_request_spec.rb
163
172
  - spec/ruact/query_spec.rb
164
173
  - spec/ruact/railtie_spec.rb
@@ -174,6 +183,7 @@ files:
174
183
  - spec/ruact/server_functions/codegen_spec.rb
175
184
  - spec/ruact/server_functions/error_payload_spec.rb
176
185
  - spec/ruact/server_functions/error_suggestion_spec.rb
186
+ - spec/ruact/server_functions/introspection_spec.rb
177
187
  - spec/ruact/server_functions/name_bridge_spec.rb
178
188
  - spec/ruact/server_functions/query_context_spec.rb
179
189
  - spec/ruact/server_functions/query_source_spec.rb
@@ -187,6 +197,9 @@ files:
187
197
  - spec/ruact/server_upload_request_spec.rb
188
198
  - spec/ruact/signed_references_spec.rb
189
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
190
203
  - spec/ruact/validation_errors_spec.rb
191
204
  - spec/ruact/view_helper_spec.rb
192
205
  - spec/spec_helper.rb
@@ -204,6 +217,7 @@ files:
204
217
  - vendor/javascript/vite-plugin-ruact/bootstrap.test.mjs
205
218
  - vendor/javascript/vite-plugin-ruact/index.js
206
219
  - vendor/javascript/vite-plugin-ruact/manifest-contract.test.mjs
220
+ - vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs
207
221
  - vendor/javascript/vite-plugin-ruact/package-lock.json
208
222
  - vendor/javascript/vite-plugin-ruact/package.json
209
223
  - vendor/javascript/vite-plugin-ruact/registry.test.mjs
@@ -215,6 +229,7 @@ files:
215
229
  - vendor/javascript/vite-plugin-ruact/tsconfig.json
216
230
  - vendor/javascript/vite-plugin-ruact/tsconfig.scaffold-agnostic.json
217
231
  - vendor/javascript/vite-plugin-ruact/tsconfig.scaffold.json
232
+ - vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts
218
233
  - vendor/javascript/vite-plugin-ruact/type-tests/emitted-module.test-d.ts
219
234
  - vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostDeleteDialog.tsx
220
235
  - vendor/javascript/vite-plugin-ruact/type-tests/scaffold/PostForm.tsx