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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.codecov.yml +31 -0
  3. data/.github/workflows/ci.yml +160 -94
  4. data/.github/workflows/server-functions-bench.yml +54 -0
  5. data/.rubocop.yml +19 -1
  6. data/.rubocop_todo.yml +175 -0
  7. data/CHANGELOG.md +86 -5
  8. data/README.md +2 -0
  9. data/RELEASING.md +9 -3
  10. data/bench/server_functions_dispatch_bench.rb +309 -0
  11. data/bench/server_functions_dispatch_bench.results.md +121 -0
  12. data/docs/internal/README.md +9 -0
  13. data/docs/internal/decisions/server-functions-api.md +1680 -0
  14. data/lib/generators/ruact/install/install_generator.rb +43 -0
  15. data/lib/generators/ruact/install/templates/application.jsx.tt +1 -1
  16. data/lib/generators/ruact/install/templates/initializer.rb.tt +1 -1
  17. data/lib/ruact/client_manifest.rb +125 -12
  18. data/lib/ruact/configuration.rb +264 -23
  19. data/lib/ruact/controller.rb +459 -32
  20. data/lib/ruact/doctor.rb +34 -2
  21. data/lib/ruact/erb_preprocessor.rb +6 -6
  22. data/lib/ruact/errors.rb +89 -0
  23. data/lib/ruact/flight/serializer.rb +2 -2
  24. data/lib/ruact/html_converter.rb +131 -31
  25. data/lib/ruact/query.rb +107 -0
  26. data/lib/ruact/railtie.rb +220 -3
  27. data/lib/ruact/render_context.rb +30 -0
  28. data/lib/ruact/render_pipeline.rb +201 -59
  29. data/lib/ruact/routing.rb +81 -0
  30. data/lib/ruact/serializable.rb +11 -11
  31. data/lib/ruact/server.rb +341 -0
  32. data/lib/ruact/server_action.rb +131 -0
  33. data/lib/ruact/server_functions/backtrace_cleaner.rb +32 -0
  34. data/lib/ruact/server_functions/bucket_two_payload.rb +109 -0
  35. data/lib/ruact/server_functions/codegen.rb +330 -0
  36. data/lib/ruact/server_functions/codegen_v2.rb +176 -0
  37. data/lib/ruact/server_functions/endpoint_controller.rb +237 -0
  38. data/lib/ruact/server_functions/error_payload.rb +93 -0
  39. data/lib/ruact/server_functions/error_rendering.rb +188 -0
  40. data/lib/ruact/server_functions/error_suggestion.rb +38 -0
  41. data/lib/ruact/server_functions/name_bridge.rb +113 -0
  42. data/lib/ruact/server_functions/query_context.rb +62 -0
  43. data/lib/ruact/server_functions/query_dispatch.rb +248 -0
  44. data/lib/ruact/server_functions/registry.rb +148 -0
  45. data/lib/ruact/server_functions/registry_entry.rb +26 -0
  46. data/lib/ruact/server_functions/route_source.rb +201 -0
  47. data/lib/ruact/server_functions/snapshot.rb +195 -0
  48. data/lib/ruact/server_functions/snapshot_writer.rb +65 -0
  49. data/lib/ruact/server_functions/standalone_context.rb +103 -0
  50. data/lib/ruact/server_functions/standalone_dispatcher.rb +178 -0
  51. data/lib/ruact/server_functions.rb +75 -0
  52. data/lib/ruact/version.rb +1 -1
  53. data/lib/ruact/view_helper.rb +17 -9
  54. data/lib/ruact.rb +85 -6
  55. data/lib/rubocop/cop/ruact/no_shared_state.rb +1 -1
  56. data/lib/tasks/benchmark.rake +15 -11
  57. data/lib/tasks/ruact.rake +81 -0
  58. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +1 -1
  59. data/spec/fixtures/flight/README.md +55 -7
  60. data/spec/fixtures/flight/bigint.txt +1 -0
  61. data/spec/fixtures/flight/infinity.txt +1 -0
  62. data/spec/fixtures/flight/nan.txt +1 -0
  63. data/spec/fixtures/flight/negative_infinity.txt +1 -0
  64. data/spec/fixtures/flight/undefined.txt +1 -0
  65. data/spec/fixtures/story_7_9_views/controller_request_spec_support/demo/show.html.erb +3 -0
  66. data/spec/ruact/client_manifest_spec.rb +108 -0
  67. data/spec/ruact/configuration_spec.rb +501 -0
  68. data/spec/ruact/controller_request_spec.rb +204 -0
  69. data/spec/ruact/controller_spec.rb +427 -39
  70. data/spec/ruact/doctor_spec.rb +118 -0
  71. data/spec/ruact/erb_preprocessor_hook_spec.rb +3 -3
  72. data/spec/ruact/erb_preprocessor_spec.rb +7 -7
  73. data/spec/ruact/errors_spec.rb +95 -0
  74. data/spec/ruact/flight/renderer_spec.rb +14 -3
  75. data/spec/ruact/flight/serializer_spec.rb +129 -88
  76. data/spec/ruact/html_converter_spec.rb +183 -5
  77. data/spec/ruact/install_generator_spec.rb +93 -0
  78. data/spec/ruact/query_request_spec.rb +446 -0
  79. data/spec/ruact/query_spec.rb +105 -0
  80. data/spec/ruact/railtie_spec.rb +2 -3
  81. data/spec/ruact/render_context_spec.rb +58 -0
  82. data/spec/ruact/render_pipeline_concurrency_spec.rb +78 -0
  83. data/spec/ruact/render_pipeline_spec.rb +784 -330
  84. data/spec/ruact/serializable_spec.rb +8 -8
  85. data/spec/ruact/server_bucket_request_spec.rb +352 -0
  86. data/spec/ruact/server_function_name_spec.rb +53 -0
  87. data/spec/ruact/server_functions/backtrace_cleaner_spec.rb +63 -0
  88. data/spec/ruact/server_functions/bucket_two_payload_spec.rb +200 -0
  89. data/spec/ruact/server_functions/codegen_spec.rb +429 -0
  90. data/spec/ruact/server_functions/csrf_request_spec.rb +380 -0
  91. data/spec/ruact/server_functions/dispatch_request_spec.rb +819 -0
  92. data/spec/ruact/server_functions/error_payload_spec.rb +222 -0
  93. data/spec/ruact/server_functions/error_suggestion_spec.rb +79 -0
  94. data/spec/ruact/server_functions/name_bridge_spec.rb +188 -0
  95. data/spec/ruact/server_functions/query_context_spec.rb +72 -0
  96. data/spec/ruact/server_functions/railtie_integration_spec.rb +345 -0
  97. data/spec/ruact/server_functions/rake_spec.rb +86 -0
  98. data/spec/ruact/server_functions/registry_spec.rb +199 -0
  99. data/spec/ruact/server_functions/route_source_spec.rb +202 -0
  100. data/spec/ruact/server_functions/snapshot_spec.rb +256 -0
  101. data/spec/ruact/server_functions/snapshot_writer_spec.rb +71 -0
  102. data/spec/ruact/server_functions/standalone_action_spec.rb +224 -0
  103. data/spec/ruact/server_functions/standalone_context_spec.rb +142 -0
  104. data/spec/ruact/server_functions/standalone_dispatcher_spec.rb +273 -0
  105. data/spec/ruact/server_rescue_request_spec.rb +416 -0
  106. data/spec/ruact/server_spec.rb +180 -0
  107. data/spec/ruact/server_upload_request_spec.rb +311 -0
  108. data/spec/ruact/view_helper_spec.rb +23 -17
  109. data/spec/spec_helper.rb +52 -1
  110. data/spec/support/fixtures/pixel.png +0 -0
  111. data/spec/support/flight_wire_parser.rb +135 -0
  112. data/spec/support/flight_wire_parser_spec.rb +93 -0
  113. data/spec/support/matchers/flight_fixture_matcher.rb +356 -0
  114. data/spec/support/matchers/flight_fixture_matcher_spec.rb +250 -0
  115. data/spec/support/rails_stub.rb +75 -5
  116. data/vendor/javascript/ruact-server-functions-runtime/index.d.ts +139 -0
  117. data/vendor/javascript/ruact-server-functions-runtime/index.js +438 -0
  118. data/vendor/javascript/ruact-server-functions-runtime/index.test.mjs +827 -0
  119. data/vendor/javascript/ruact-server-functions-runtime/package.json +22 -0
  120. data/vendor/javascript/vite-plugin-ruact/index.js +164 -0
  121. data/vendor/javascript/vite-plugin-ruact/package-lock.json +1429 -0
  122. data/vendor/javascript/vite-plugin-ruact/package.json +15 -0
  123. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.mjs +761 -0
  124. data/vendor/javascript/vite-plugin-ruact/server-functions-codegen.test.mjs +866 -0
  125. data/vendor/javascript/vite-plugin-ruact/vitest.config.mjs +15 -0
  126. metadata +88 -5
  127. data/lib/ruact/component_registry.rb +0 -31
  128. 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
+ }