ruact 0.0.2 → 0.0.3
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/.codecov.yml +31 -0
- data/.github/workflows/ci.yml +160 -94
- data/.github/workflows/server-functions-bench.yml +54 -0
- data/.rubocop.yml +19 -1
- data/.rubocop_todo.yml +175 -0
- data/CHANGELOG.md +86 -5
- data/README.md +2 -0
- data/RELEASING.md +9 -3
- data/bench/server_functions_dispatch_bench.rb +309 -0
- data/bench/server_functions_dispatch_bench.results.md +121 -0
- data/docs/internal/README.md +9 -0
- data/docs/internal/decisions/server-functions-api.md +1680 -0
- data/lib/generators/ruact/install/install_generator.rb +43 -0
- data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
- data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
- data/lib/ruact/client_manifest.rb +125 -12
- data/lib/ruact/configuration.rb +264 -23
- data/lib/ruact/controller.rb +459 -32
- data/lib/ruact/doctor.rb +34 -2
- data/lib/ruact/erb_preprocessor.rb +6 -6
- data/lib/ruact/errors.rb +89 -0
- data/lib/ruact/flight/serializer.rb +2 -2
- data/lib/ruact/html_converter.rb +131 -31
- data/lib/ruact/query.rb +107 -0
- data/lib/ruact/railtie.rb +220 -3
- data/lib/ruact/render_context.rb +30 -0
- data/lib/ruact/render_pipeline.rb +201 -59
- data/lib/ruact/routing.rb +81 -0
- data/lib/ruact/serializable.rb +11 -11
- data/lib/ruact/server.rb +341 -0
- data/lib/ruact/server_action.rb +131 -0
- data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
- data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
- data/lib/ruact/server_functions/codegen.rb +330 -0
- data/lib/ruact/server_functions/codegen_v2.rb +176 -0
- data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
- data/lib/ruact/server_functions/error_payload.rb +93 -0
- data/lib/ruact/server_functions/error_rendering.rb +188 -0
- data/lib/ruact/server_functions/error_suggestion.rb +38 -0
- data/lib/ruact/server_functions/name_bridge.rb +113 -0
- data/lib/ruact/server_functions/query_context.rb +62 -0
- data/lib/ruact/server_functions/query_dispatch.rb +248 -0
- data/lib/ruact/server_functions/registry.rb +148 -0
- data/lib/ruact/server_functions/registry_entry.rb +26 -0
- data/lib/ruact/server_functions/route_source.rb +201 -0
- data/lib/ruact/server_functions/snapshot.rb +195 -0
- data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
- data/lib/ruact/server_functions/standalone_context.rb +103 -0
- data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
- data/lib/ruact/server_functions.rb +75 -0
- data/lib/ruact/version.rb +1 -1
- data/lib/ruact/view_helper.rb +17 -9
- data/lib/ruact.rb +85 -6
- data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
- data/lib/tasks/benchmark.rake +15 -11
- data/lib/tasks/ruact.rake +81 -0
- data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
- data/spec/fixtures/flight/README.md +55 -7
- data/spec/fixtures/flight/bigint.txt +1 -0
- data/spec/fixtures/flight/infinity.txt +1 -0
- data/spec/fixtures/flight/nan.txt +1 -0
- data/spec/fixtures/flight/negative_infinity.txt +1 -0
- data/spec/fixtures/flight/undefined.txt +1 -0
- data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
- data/spec/ruact/client_manifest_spec.rb +108 -0
- data/spec/ruact/configuration_spec.rb +501 -0
- data/spec/ruact/controller_request_spec.rb +204 -0
- data/spec/ruact/controller_spec.rb +427 -39
- data/spec/ruact/doctor_spec.rb +118 -0
- data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
- data/spec/ruact/erb_preprocessor_spec.rb +7 -7
- data/spec/ruact/errors_spec.rb +95 -0
- data/spec/ruact/flight/renderer_spec.rb +14 -3
- data/spec/ruact/flight/serializer_spec.rb +129 -88
- data/spec/ruact/html_converter_spec.rb +183 -5
- data/spec/ruact/install_generator_spec.rb +93 -0
- data/spec/ruact/query_request_spec.rb +446 -0
- data/spec/ruact/query_spec.rb +105 -0
- data/spec/ruact/railtie_spec.rb +2 -3
- data/spec/ruact/render_context_spec.rb +58 -0
- data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
- data/spec/ruact/render_pipeline_spec.rb +784 -330
- data/spec/ruact/serializable_spec.rb +8 -8
- data/spec/ruact/server_bucket_request_spec.rb +352 -0
- data/spec/ruact/server_function_name_spec.rb +53 -0
- data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
- data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
- data/spec/ruact/server_functions/codegen_spec.rb +429 -0
- data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
- data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
- data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
- data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
- data/spec/ruact/server_functions/name_bridge_spec.rb +188 -0
- data/spec/ruact/server_functions/query_context_spec.rb +72 -0
- data/spec/ruact/server_functions/railtie_integration_spec.rb +345 -0
- data/spec/ruact/server_functions/rake_spec.rb +86 -0
- data/spec/ruact/server_functions/registry_spec.rb +199 -0
- data/spec/ruact/server_functions/route_source_spec.rb +202 -0
- data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
- data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
- data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
- data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
- data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
- data/spec/ruact/server_rescue_request_spec.rb +416 -0
- data/spec/ruact/server_spec.rb +180 -0
- data/spec/ruact/server_upload_request_spec.rb +311 -0
- data/spec/ruact/view_helper_spec.rb +23 -17
- data/spec/spec_helper.rb +52 -1
- data/spec/support/fixtures/pixel.png +0 -0
- data/spec/support/flight_wire_parser.rb +135 -0
- data/spec/support/flight_wire_parser_spec.rb +93 -0
- data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
- data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
- data/spec/support/rails_stub.rb +75 -5
- data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +139 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.js +438 -0
- data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
- data/vendor/javascript/ruact-server-functions-runtime/package.json +22 -0
- data/vendor/javascript/vite-plugin-ruact/index.js +164 -0
- data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
- data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +761 -0
- data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +866 -0
- data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +15 -0
- metadata +88 -5
- data/lib/ruact/component_registry.rb +0 -31
- data/lib/tasks/rsc.rake +0 -9
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ruact-server-functions-runtime",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Server-functions runtime for ruact gem (Stories 8.1 + 8.2). Provides `_makeRef(name)` (POSTs to `/__ruact/fn/:name` with CSRF + JSON / FormData support, including the useActionState two-arg shape) and `revalidate(path?)` (Flight refetch via the installed ruact-router).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"private": true,
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"vitest": "^2.1.9"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { installServerFunctionsHooks } from "./server-functions-codegen.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* vite-plugin-ruact
|
|
7
|
+
*
|
|
8
|
+
* Scans app/javascript/components for files with "use client" directives and
|
|
9
|
+
* emits public/react-client-manifest.json so the Rails gem can resolve
|
|
10
|
+
* component names to chunk URLs.
|
|
11
|
+
*
|
|
12
|
+
* Manifest format:
|
|
13
|
+
* {
|
|
14
|
+
* "LikeButton": {
|
|
15
|
+
* "id": "/assets/LikeButton-abc123.js",
|
|
16
|
+
* "name": "LikeButton",
|
|
17
|
+
* "chunks": ["/assets/LikeButton-abc123.js"]
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export default function ruact(options = {}) {
|
|
22
|
+
const {
|
|
23
|
+
componentsDir = "app/javascript/components",
|
|
24
|
+
manifestOutput = "public/react-client-manifest.json",
|
|
25
|
+
} = options;
|
|
26
|
+
|
|
27
|
+
let root;
|
|
28
|
+
let manifest = {};
|
|
29
|
+
|
|
30
|
+
return installServerFunctionsHooks({
|
|
31
|
+
name: "vite-plugin-ruact",
|
|
32
|
+
|
|
33
|
+
configResolved(config) {
|
|
34
|
+
root = config.root;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// During dev: build the manifest from source files
|
|
38
|
+
buildStart() {
|
|
39
|
+
manifest = buildManifest(path.resolve(root, componentsDir));
|
|
40
|
+
writeManifest(path.resolve(root, manifestOutput), manifest);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// During build: update with hashed chunk URLs from the bundle
|
|
44
|
+
generateBundle(_options, bundle) {
|
|
45
|
+
const updated = {};
|
|
46
|
+
|
|
47
|
+
for (const [chunkFileName, chunk] of Object.entries(bundle)) {
|
|
48
|
+
if (chunk.type !== "chunk") continue;
|
|
49
|
+
|
|
50
|
+
const facadeId = chunk.facadeModuleId;
|
|
51
|
+
if (!facadeId) continue;
|
|
52
|
+
|
|
53
|
+
// Find manifest entries whose source file matches this chunk
|
|
54
|
+
for (const [name, entry] of Object.entries(manifest)) {
|
|
55
|
+
if (facadeId === entry._sourceFile) {
|
|
56
|
+
const url = "/" + chunkFileName;
|
|
57
|
+
updated[name] = {
|
|
58
|
+
id: url,
|
|
59
|
+
name,
|
|
60
|
+
chunks: [url],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Merge: keep entries that didn't get a hashed URL (dev mode)
|
|
67
|
+
const final = { ...manifest, ...updated };
|
|
68
|
+
// Strip internal _sourceFile field
|
|
69
|
+
for (const entry of Object.values(final)) {
|
|
70
|
+
delete entry._sourceFile;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeManifest(path.resolve(root, manifestOutput), final);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Dev server: watch components dir and rebuild manifest on change
|
|
77
|
+
configureServer(server) {
|
|
78
|
+
const dir = path.resolve(root, componentsDir);
|
|
79
|
+
server.watcher.add(dir);
|
|
80
|
+
server.watcher.on("change", (file) => {
|
|
81
|
+
if (file.startsWith(dir)) {
|
|
82
|
+
manifest = buildManifest(dir);
|
|
83
|
+
writeManifest(path.resolve(root, manifestOutput), manifest);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
}, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildManifest(componentsDir) {
|
|
91
|
+
const manifest = {};
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(componentsDir)) return manifest;
|
|
94
|
+
|
|
95
|
+
const files = walkDir(componentsDir).filter((f) =>
|
|
96
|
+
/\.(jsx?|tsx?)$/.test(f)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
for (const file of files) {
|
|
100
|
+
const content = fs.readFileSync(file, "utf8");
|
|
101
|
+
if (!hasUseClient(content)) continue;
|
|
102
|
+
|
|
103
|
+
const exports = extractExportNames(content);
|
|
104
|
+
const relUrl = "/" + path.relative(componentsDir, file);
|
|
105
|
+
|
|
106
|
+
for (const name of exports) {
|
|
107
|
+
manifest[name] = {
|
|
108
|
+
id: relUrl,
|
|
109
|
+
name,
|
|
110
|
+
chunks: [relUrl],
|
|
111
|
+
_sourceFile: file, // used during build to match hashed chunks
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return manifest;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasUseClient(content) {
|
|
120
|
+
// "use client" must appear as a directive at the top of the file
|
|
121
|
+
return /^\s*["']use client["']/m.test(content);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function extractExportNames(content) {
|
|
125
|
+
const names = new Set();
|
|
126
|
+
|
|
127
|
+
// export function Foo
|
|
128
|
+
// export const Foo
|
|
129
|
+
// export class Foo
|
|
130
|
+
const namedRe = /export\s+(?:default\s+)?(?:function|const|class|let|var)\s+([A-Z][A-Za-z0-9]*)/g;
|
|
131
|
+
let m;
|
|
132
|
+
while ((m = namedRe.exec(content)) !== null) {
|
|
133
|
+
names.add(m[1]);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// export { Foo, Bar }
|
|
137
|
+
const bracedRe = /export\s+\{([^}]+)\}/g;
|
|
138
|
+
while ((m = bracedRe.exec(content)) !== null) {
|
|
139
|
+
for (const part of m[1].split(",")) {
|
|
140
|
+
const name = part.trim().split(/\s+as\s+/).pop().trim();
|
|
141
|
+
if (/^[A-Z]/.test(name)) names.add(name);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Array.from(names);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function writeManifest(outputPath, manifest) {
|
|
149
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
150
|
+
fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function walkDir(dir) {
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
156
|
+
const full = path.join(dir, entry.name);
|
|
157
|
+
if (entry.isDirectory()) {
|
|
158
|
+
results.push(...walkDir(full));
|
|
159
|
+
} else {
|
|
160
|
+
results.push(full);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|