ruact 0.0.1

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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +166 -0
  3. data/.rubocop.yml +89 -0
  4. data/CHANGELOG.md +32 -0
  5. data/README.md +35 -0
  6. data/RELEASING.md +203 -0
  7. data/Rakefile +10 -0
  8. data/SECURITY.md +62 -0
  9. data/Users/luiz/workspace/rails-rsc/gem/vendor/javascript/vite-plugin-ruact/index.js +163 -0
  10. data/lib/generators/ruact/install/install_generator.rb +100 -0
  11. data/lib/generators/ruact/install/templates/application.jsx.tt +51 -0
  12. data/lib/generators/ruact/install/templates/initializer.rb.tt +18 -0
  13. data/lib/generators/ruact/install/templates/vite.config.js.tt +26 -0
  14. data/lib/ruact/client_manifest.rb +115 -0
  15. data/lib/ruact/component_registry.rb +31 -0
  16. data/lib/ruact/configuration.rb +32 -0
  17. data/lib/ruact/controller.rb +195 -0
  18. data/lib/ruact/doctor.rb +84 -0
  19. data/lib/ruact/erb_preprocessor.rb +120 -0
  20. data/lib/ruact/erb_preprocessor_hook.rb +20 -0
  21. data/lib/ruact/errors.rb +14 -0
  22. data/lib/ruact/flight/react_element.rb +40 -0
  23. data/lib/ruact/flight/renderer.rb +73 -0
  24. data/lib/ruact/flight/request.rb +54 -0
  25. data/lib/ruact/flight/row_emitter.rb +37 -0
  26. data/lib/ruact/flight/serializer.rb +215 -0
  27. data/lib/ruact/flight.rb +12 -0
  28. data/lib/ruact/html_converter.rb +159 -0
  29. data/lib/ruact/railtie.rb +99 -0
  30. data/lib/ruact/render_pipeline.rb +107 -0
  31. data/lib/ruact/serializable.rb +58 -0
  32. data/lib/ruact/version.rb +5 -0
  33. data/lib/ruact/view_helper.rb +23 -0
  34. data/lib/ruact.rb +48 -0
  35. data/lib/rubocop/cop/ruact/no_extend_self.rb +46 -0
  36. data/lib/rubocop/cop/ruact/no_io_in_flight.rb +72 -0
  37. data/lib/rubocop/cop/ruact/no_shared_state.rb +49 -0
  38. data/lib/rubocop/cop/ruact.rb +5 -0
  39. data/lib/tasks/benchmark.rake +70 -0
  40. data/lib/tasks/rsc.rake +9 -0
  41. data/sig/ruact.rbs +4 -0
  42. data/spec/benchmarks/baseline.json +1 -0
  43. data/spec/benchmarks/render_pipeline_benchmark_spec.rb +92 -0
  44. data/spec/fixtures/flight/README.md +88 -0
  45. data/spec/fixtures/flight/array.txt +1 -0
  46. data/spec/fixtures/flight/as_json_object.txt +2 -0
  47. data/spec/fixtures/flight/boolean_false.txt +1 -0
  48. data/spec/fixtures/flight/boolean_true.txt +1 -0
  49. data/spec/fixtures/flight/client_component_with_props.txt +2 -0
  50. data/spec/fixtures/flight/client_reference.txt +2 -0
  51. data/spec/fixtures/flight/hash.txt +1 -0
  52. data/spec/fixtures/flight/nil.txt +1 -0
  53. data/spec/fixtures/flight/number_float.txt +1 -0
  54. data/spec/fixtures/flight/number_integer.txt +1 -0
  55. data/spec/fixtures/flight/react_element_no_props.txt +1 -0
  56. data/spec/fixtures/flight/redirect_row.txt +1 -0
  57. data/spec/fixtures/flight/serializable_object.txt +2 -0
  58. data/spec/fixtures/flight/string_basic.txt +1 -0
  59. data/spec/fixtures/flight/string_dollar_escape.txt +1 -0
  60. data/spec/ruact/client_manifest_spec.rb +126 -0
  61. data/spec/ruact/controller_spec.rb +213 -0
  62. data/spec/ruact/doctor_spec.rb +234 -0
  63. data/spec/ruact/erb_preprocessor_hook_spec.rb +52 -0
  64. data/spec/ruact/erb_preprocessor_spec.rb +89 -0
  65. data/spec/ruact/errors_spec.rb +43 -0
  66. data/spec/ruact/flight/renderer_spec.rb +122 -0
  67. data/spec/ruact/flight/serializer_spec.rb +453 -0
  68. data/spec/ruact/html_converter_spec.rb +147 -0
  69. data/spec/ruact/install_generator_spec.rb +212 -0
  70. data/spec/ruact/railtie_spec.rb +156 -0
  71. data/spec/ruact/render_pipeline_spec.rb +474 -0
  72. data/spec/ruact/serializable_spec.rb +53 -0
  73. data/spec/ruact/view_helper_spec.rb +46 -0
  74. data/spec/spec_helper.rb +16 -0
  75. data/spec/support/matchers/flight_fixture_matcher.rb +25 -0
  76. data/spec/support/rails_stub.rb +45 -0
  77. data/vendor/javascript/vite-plugin-ruact/index.js +163 -0
  78. metadata +136 -0
@@ -0,0 +1,163 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /**
5
+ * vite-plugin-ruact
6
+ *
7
+ * Scans app/javascript/components for files with "use client" directives and
8
+ * emits public/react-client-manifest.json so the Rails gem can resolve
9
+ * component names to chunk URLs.
10
+ *
11
+ * Manifest format:
12
+ * {
13
+ * "LikeButton": {
14
+ * "id": "/assets/LikeButton-abc123.js",
15
+ * "name": "LikeButton",
16
+ * "chunks": ["/assets/LikeButton-abc123.js"]
17
+ * }
18
+ * }
19
+ */
20
+ export default function ruact(options = {}) {
21
+ const {
22
+ componentsDir = "app/javascript/components",
23
+ manifestOutput = "public/react-client-manifest.json",
24
+ } = options;
25
+
26
+ let root;
27
+ let manifest = {};
28
+
29
+ return {
30
+ name: "vite-plugin-ruact",
31
+
32
+ configResolved(config) {
33
+ root = config.root;
34
+ },
35
+
36
+ // During dev: build the manifest from source files
37
+ buildStart() {
38
+ manifest = buildManifest(path.resolve(root, componentsDir));
39
+ writeManifest(path.resolve(root, manifestOutput), manifest);
40
+ },
41
+
42
+ // During build: update with hashed chunk URLs from the bundle
43
+ generateBundle(_options, bundle) {
44
+ const updated = {};
45
+
46
+ for (const [chunkFileName, chunk] of Object.entries(bundle)) {
47
+ if (chunk.type !== "chunk") continue;
48
+
49
+ const facadeId = chunk.facadeModuleId;
50
+ if (!facadeId) continue;
51
+
52
+ // Find manifest entries whose source file matches this chunk
53
+ for (const [name, entry] of Object.entries(manifest)) {
54
+ if (facadeId === entry._sourceFile) {
55
+ const url = "/" + chunkFileName;
56
+ updated[name] = {
57
+ id: url,
58
+ name,
59
+ chunks: [url],
60
+ };
61
+ }
62
+ }
63
+ }
64
+
65
+ // Merge: keep entries that didn't get a hashed URL (dev mode)
66
+ const final = { ...manifest, ...updated };
67
+ // Strip internal _sourceFile field
68
+ for (const entry of Object.values(final)) {
69
+ delete entry._sourceFile;
70
+ }
71
+
72
+ writeManifest(path.resolve(root, manifestOutput), final);
73
+ },
74
+
75
+ // Dev server: watch components dir and rebuild manifest on change
76
+ configureServer(server) {
77
+ const dir = path.resolve(root, componentsDir);
78
+ server.watcher.add(dir);
79
+ server.watcher.on("change", (file) => {
80
+ if (file.startsWith(dir)) {
81
+ manifest = buildManifest(dir);
82
+ writeManifest(path.resolve(root, manifestOutput), manifest);
83
+ }
84
+ });
85
+ },
86
+ };
87
+ }
88
+
89
+ function buildManifest(componentsDir) {
90
+ const manifest = {};
91
+
92
+ if (!fs.existsSync(componentsDir)) return manifest;
93
+
94
+ const files = walkDir(componentsDir).filter((f) =>
95
+ /\.(jsx?|tsx?)$/.test(f)
96
+ );
97
+
98
+ for (const file of files) {
99
+ const content = fs.readFileSync(file, "utf8");
100
+ if (!hasUseClient(content)) continue;
101
+
102
+ const exports = extractExportNames(content);
103
+ const relUrl = "/" + path.relative(componentsDir, file);
104
+
105
+ for (const name of exports) {
106
+ manifest[name] = {
107
+ id: relUrl,
108
+ name,
109
+ chunks: [relUrl],
110
+ _sourceFile: file, // used during build to match hashed chunks
111
+ };
112
+ }
113
+ }
114
+
115
+ return manifest;
116
+ }
117
+
118
+ function hasUseClient(content) {
119
+ // "use client" must appear as a directive at the top of the file
120
+ return /^\s*["']use client["']/m.test(content);
121
+ }
122
+
123
+ function extractExportNames(content) {
124
+ const names = new Set();
125
+
126
+ // export function Foo
127
+ // export const Foo
128
+ // export class Foo
129
+ const namedRe = /export\s+(?:default\s+)?(?:function|const|class|let|var)\s+([A-Z][A-Za-z0-9]*)/g;
130
+ let m;
131
+ while ((m = namedRe.exec(content)) !== null) {
132
+ names.add(m[1]);
133
+ }
134
+
135
+ // export { Foo, Bar }
136
+ const bracedRe = /export\s+\{([^}]+)\}/g;
137
+ while ((m = bracedRe.exec(content)) !== null) {
138
+ for (const part of m[1].split(",")) {
139
+ const name = part.trim().split(/\s+as\s+/).pop().trim();
140
+ if (/^[A-Z]/.test(name)) names.add(name);
141
+ }
142
+ }
143
+
144
+ return Array.from(names);
145
+ }
146
+
147
+ function writeManifest(outputPath, manifest) {
148
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
149
+ fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
150
+ }
151
+
152
+ function walkDir(dir) {
153
+ const results = [];
154
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
155
+ const full = path.join(dir, entry.name);
156
+ if (entry.isDirectory()) {
157
+ results.push(...walkDir(full));
158
+ } else {
159
+ results.push(full);
160
+ }
161
+ }
162
+ return results;
163
+ }
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruact
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Luiz Garcia
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: nokogiri
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.15'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.15'
26
+ email:
27
+ - luizcg@gmail.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - ".github/workflows/ci.yml"
33
+ - ".rubocop.yml"
34
+ - "/Users/luiz/workspace/rails-rsc/gem/vendor/javascript/vite-plugin-ruact/index.js"
35
+ - CHANGELOG.md
36
+ - README.md
37
+ - RELEASING.md
38
+ - Rakefile
39
+ - SECURITY.md
40
+ - lib/generators/ruact/install/install_generator.rb
41
+ - lib/generators/ruact/install/templates/application.jsx.tt
42
+ - lib/generators/ruact/install/templates/initializer.rb.tt
43
+ - lib/generators/ruact/install/templates/vite.config.js.tt
44
+ - lib/ruact.rb
45
+ - lib/ruact/client_manifest.rb
46
+ - lib/ruact/component_registry.rb
47
+ - lib/ruact/configuration.rb
48
+ - lib/ruact/controller.rb
49
+ - lib/ruact/doctor.rb
50
+ - lib/ruact/erb_preprocessor.rb
51
+ - lib/ruact/erb_preprocessor_hook.rb
52
+ - lib/ruact/errors.rb
53
+ - lib/ruact/flight.rb
54
+ - lib/ruact/flight/react_element.rb
55
+ - lib/ruact/flight/renderer.rb
56
+ - lib/ruact/flight/request.rb
57
+ - lib/ruact/flight/row_emitter.rb
58
+ - lib/ruact/flight/serializer.rb
59
+ - lib/ruact/html_converter.rb
60
+ - lib/ruact/railtie.rb
61
+ - lib/ruact/render_pipeline.rb
62
+ - lib/ruact/serializable.rb
63
+ - lib/ruact/version.rb
64
+ - lib/ruact/view_helper.rb
65
+ - lib/rubocop/cop/ruact.rb
66
+ - lib/rubocop/cop/ruact/no_extend_self.rb
67
+ - lib/rubocop/cop/ruact/no_io_in_flight.rb
68
+ - lib/rubocop/cop/ruact/no_shared_state.rb
69
+ - lib/tasks/benchmark.rake
70
+ - lib/tasks/rsc.rake
71
+ - sig/ruact.rbs
72
+ - spec/benchmarks/baseline.json
73
+ - spec/benchmarks/render_pipeline_benchmark_spec.rb
74
+ - spec/fixtures/flight/README.md
75
+ - spec/fixtures/flight/array.txt
76
+ - spec/fixtures/flight/as_json_object.txt
77
+ - spec/fixtures/flight/boolean_false.txt
78
+ - spec/fixtures/flight/boolean_true.txt
79
+ - spec/fixtures/flight/client_component_with_props.txt
80
+ - spec/fixtures/flight/client_reference.txt
81
+ - spec/fixtures/flight/hash.txt
82
+ - spec/fixtures/flight/nil.txt
83
+ - spec/fixtures/flight/number_float.txt
84
+ - spec/fixtures/flight/number_integer.txt
85
+ - spec/fixtures/flight/react_element_no_props.txt
86
+ - spec/fixtures/flight/redirect_row.txt
87
+ - spec/fixtures/flight/serializable_object.txt
88
+ - spec/fixtures/flight/string_basic.txt
89
+ - spec/fixtures/flight/string_dollar_escape.txt
90
+ - spec/ruact/client_manifest_spec.rb
91
+ - spec/ruact/controller_spec.rb
92
+ - spec/ruact/doctor_spec.rb
93
+ - spec/ruact/erb_preprocessor_hook_spec.rb
94
+ - spec/ruact/erb_preprocessor_spec.rb
95
+ - spec/ruact/errors_spec.rb
96
+ - spec/ruact/flight/renderer_spec.rb
97
+ - spec/ruact/flight/serializer_spec.rb
98
+ - spec/ruact/html_converter_spec.rb
99
+ - spec/ruact/install_generator_spec.rb
100
+ - spec/ruact/railtie_spec.rb
101
+ - spec/ruact/render_pipeline_spec.rb
102
+ - spec/ruact/serializable_spec.rb
103
+ - spec/ruact/view_helper_spec.rb
104
+ - spec/spec_helper.rb
105
+ - spec/support/matchers/flight_fixture_matcher.rb
106
+ - spec/support/rails_stub.rb
107
+ - vendor/javascript/vite-plugin-ruact/index.js
108
+ homepage: https://luizcg.github.io/ruact/
109
+ licenses:
110
+ - MIT
111
+ metadata:
112
+ allowed_push_host: https://rubygems.org
113
+ homepage_uri: https://luizcg.github.io/ruact/
114
+ source_code_uri: https://github.com/luizcg/ruact
115
+ changelog_uri: https://github.com/luizcg/ruact/blob/main/CHANGELOG.md
116
+ bug_tracker_uri: https://github.com/luizcg/ruact/issues
117
+ rubygems_mfa_required: 'true'
118
+ rdoc_options: []
119
+ require_paths:
120
+ - lib
121
+ required_ruby_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: 3.2.0
126
+ required_rubygems_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubygems_version: 3.7.2
133
+ specification_version: 4
134
+ summary: React Server Components for Rails — render React components from ERB using
135
+ the Flight wire format.
136
+ test_files: []