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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -1
- 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 +18 -3
- data/lib/ruact/doctor.rb +67 -9
- data/lib/ruact/erb_preprocessor.rb +120 -1
- data/lib/ruact/errors.rb +16 -0
- data/lib/ruact/manifest_resolver.rb +149 -0
- data/lib/ruact/railtie.rb +14 -3
- data/lib/ruact/render_pipeline.rb +8 -1
- 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/ruact/view_helper.rb +11 -4
- data/lib/ruact.rb +1 -0
- data/lib/tasks/ruact.rake +55 -2
- data/spec/ruact/controller_spec.rb +7 -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 +174 -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/spec_helper.rb +15 -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/index.js +20 -0
- data/vendor/javascript/vite-plugin-ruact/manifest-http.test.mjs +125 -0
- data/vendor/javascript/vite-plugin-ruact/type-tests/auto-revalidate.test-d.ts +25 -0
- 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.
|
|
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
|
|
@@ -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
|