tombolo 0.9.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3cacc9db2ac7d8ba235b9b142fcd8ff0b97bff68e85335c621ad305adb55839c
4
+ data.tar.gz: 04f784cf784897a241fe0b410ab4856214c8ffe6879db882028deeec8adfe591
5
+ SHA512:
6
+ metadata.gz: 456ea07d98bb2e17a5e7dd33a926383ae84612faa4ec7d9874bb8b960d6ebfc2ece64b7c90ed82af38972a7fc45a874cbc08037e577e0cd9729f0d4268fe79fd
7
+ data.tar.gz: de35b3d525d13de7535a7e57bd17312263157426ea119c3b96d1008e101c7e89cbf23cd904f21bbfd2416dd060424cb0dd92c868bcfa9a884c78a09276067420
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Inge Jørgensen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # Tombolo
2
+
3
+ Minimal React component mounting for Rails, inspired by
4
+ [react-rails](https://github.com/reactjs/react-rails).
5
+
6
+ Tombolo is designed for modern Rails apps using jsbundling-rails and
7
+ propshaft. It provides a `react_component` view helper and optional
8
+ server-side rendering with no asset pipeline integration — just a component
9
+ map and a few lines of JavaScript.
10
+
11
+ ## Installation
12
+
13
+ Add the gem to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "tombolo"
17
+ # gem "execjs" # required for server-side rendering
18
+ ```
19
+
20
+ Install the npm package:
21
+
22
+ ```sh
23
+ npm install tombolo
24
+ # or
25
+ pnpm add tombolo
26
+ ```
27
+
28
+ Run the install generator to create a config initializer and SSR entry
29
+ point:
30
+
31
+ ```sh
32
+ bin/rails generate tombolo:install
33
+ ```
34
+
35
+ ## Usage
36
+
37
+ ### Rails helper
38
+
39
+ Render a React component from any view or partial:
40
+
41
+ ```erb
42
+ <%= react_component("Greeting", props: { name: "World" }) %>
43
+ ```
44
+
45
+ ### Client-side mounting
46
+
47
+ Tombolo takes a component map — an object mapping component names to React
48
+ components. The easiest way to create one is a barrel file that re-exports
49
+ your components:
50
+
51
+ ```typescript
52
+ // app/javascript/components/index.ts
53
+ export { Greeting } from "./Greeting";
54
+ export { SearchForm } from "./SearchForm";
55
+ ```
56
+
57
+ Import it and call `Tombolo.start` to mount components on page load:
58
+
59
+ ```typescript
60
+ import * as Tombolo from "tombolo";
61
+ import * as components from "./components";
62
+
63
+ Tombolo.start(components);
64
+ ```
65
+
66
+ Both `Tombolo.mount` and `Tombolo.unmount` accept an optional `scope`
67
+ parameter to limit operations to a subtree of the DOM:
68
+
69
+ ```typescript
70
+ Tombolo.mount(components, document.getElementById("sidebar"));
71
+ ```
72
+
73
+ #### Turbo
74
+
75
+ For apps using Turbo, use `Tombolo.mount` and `Tombolo.unmount` directly:
76
+
77
+ ```typescript
78
+ import * as Tombolo from "tombolo";
79
+ import * as components from "./components";
80
+
81
+ document.addEventListener("turbo:load", () => Tombolo.mount(components));
82
+ document.addEventListener("turbo:before-cache", () => Tombolo.unmount());
83
+ ```
84
+
85
+ ### Server-side rendering
86
+
87
+ Create a server entry point that registers your components:
88
+
89
+ ```typescript
90
+ // app/javascript/prerender.ts
91
+ import { registerServerRenderer } from "tombolo/server";
92
+ import * as components from "./components";
93
+
94
+ registerServerRenderer(components);
95
+ ```
96
+
97
+ Build it with esbuild (or your bundler of choice) targeting a CommonJS
98
+ output that ExecJS can evaluate.
99
+
100
+ Then use `prerender: true` in your views:
101
+
102
+ ```erb
103
+ <%= react_component("Greeting", props: { name: "World" }, prerender: true) %>
104
+ ```
105
+
106
+ The default server bundle path is `app/assets/builds/prerender.js`. To
107
+ customize it, add an initializer:
108
+
109
+ ```ruby
110
+ # config/initializers/tombolo.rb
111
+ Tombolo.configure do |config|
112
+ config.server_bundle = "path/to/your/bundle.js"
113
+ end
114
+ ```
115
+
116
+ ## API Reference
117
+
118
+ ### npm
119
+
120
+ #### `Tombolo.start(components)`
121
+
122
+ Calls `mount` on `DOMContentLoaded`, or immediately if the DOM is already
123
+ loaded. Convenience function for apps without Turbo.
124
+
125
+ #### `Tombolo.mount(components, scope?)`
126
+
127
+ Scans `scope` (default: `document`) for elements with a
128
+ `data-react-component` attribute. For each element, looks up the component
129
+ by name, parses props from `data-react-props`, and mounts it.
130
+ Already-mounted elements are skipped. Missing components log a warning to
131
+ the console.
132
+
133
+ #### `Tombolo.unmount(scope?)`
134
+
135
+ Unmounts all tracked React roots within `scope` (default: `document`).
136
+
137
+ #### `registerServerRenderer(components)`
138
+
139
+ Assigns a `renderComponent(name, propsJson)` function to `globalThis`,
140
+ making it callable from ExecJS. Used in server entry points for SSR.
141
+
142
+ ### Ruby
143
+
144
+ #### `react_component(name, props: {}, prerender: false, camelize_props: nil)`
145
+
146
+ Renders a `<div>` with `data-react-component` and `data-react-props`
147
+ attributes. When `prerender: true`, the component is rendered on the server
148
+ via ExecJS and the HTML is placed inside the div. Pass `camelize_props: true`
149
+ to convert snake_case prop keys to camelCase, or set it globally in the
150
+ configuration.
151
+
152
+ #### `Tombolo.configure { |config| ... }`
153
+
154
+ - `config.camelize_props` — Convert snake_case prop keys to camelCase.
155
+ Default: `false`
156
+ - `config.server_bundle` — Path to the server-side JS bundle for SSR.
157
+ Default: `"app/assets/builds/prerender.js"`
158
+
159
+ ## Migrating from react-rails
160
+
161
+ The main difference is the helper signature. react-rails passes props as a
162
+ positional argument, Tombolo uses a keyword argument:
163
+
164
+ ```ruby
165
+ # react-rails
166
+ react_component("Name", { title: "Hello" })
167
+
168
+ # Tombolo
169
+ react_component("Name", props: { title: "Hello" })
170
+ ```
171
+
172
+ ## License
173
+
174
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList["test/**/*_test.rb"]
9
+ end
10
+
11
+ task default: :test
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tombolo
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ desc "Creates the Tombolo configuration"
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ def create_initializer
10
+ copy_file "initializer.rb", "config/initializers/tombolo.rb"
11
+ end
12
+
13
+ def create_prerender
14
+ copy_file "prerender.ts", "app/javascript/prerender.ts"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Tombolo.configure do |config|
4
+ # config.camelize_props = true
5
+ # config.server_bundle = "app/assets/builds/prerender.js"
6
+ end
@@ -0,0 +1,4 @@
1
+ import { registerServerRenderer } from "tombolo/server";
2
+ import * as components from "./components";
3
+
4
+ registerServerRenderer(components);
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tombolo
4
+ class Configuration
5
+ attr_accessor :camelize_props, :server_bundle
6
+
7
+ def initialize
8
+ @camelize_props = false
9
+ @server_bundle = "app/assets/builds/prerender.js"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tombolo/view_helper"
4
+
5
+ module Tombolo
6
+ class Railtie < Rails::Railtie
7
+ initializer "tombolo.view_helper" do
8
+ ActiveSupport.on_load(:action_view) do
9
+ include Tombolo::ViewHelper
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "execjs"
5
+ rescue LoadError
6
+ raise LoadError,
7
+ "ExecJS is required for server-side rendering. " \
8
+ "Add `gem \"execjs\"` to your Gemfile."
9
+ end
10
+
11
+ module Tombolo
12
+ class Renderer
13
+ def initialize
14
+ @mutex = Mutex.new
15
+ @context = nil
16
+ end
17
+
18
+ def render(name, props_json)
19
+ @mutex.synchronize do
20
+ context.call("renderComponent", name, props_json)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def context
27
+ @context ||= ExecJS.compile(bundle_source)
28
+ end
29
+
30
+ def bundle_source
31
+ path = Tombolo.configuration.server_bundle
32
+ raise "Tombolo server bundle not found: #{path}" unless File.exist?(path)
33
+
34
+ console_polyfill + File.read(path)
35
+ end
36
+
37
+ def console_polyfill
38
+ "var console = { log: function(){}, warn: function(){}, error: function(){} };\n"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tombolo
4
+ VERSION = "0.9.0"
5
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tombolo
4
+ module ViewHelper
5
+ include ActionView::Helpers::TagHelper
6
+ include ActionView::Helpers::TextHelper
7
+
8
+ def react_component(name, props: {}, prerender: false, camelize_props: nil)
9
+ camelize = camelize_props.nil? ? Tombolo.configuration.camelize_props : camelize_props
10
+ props = props.deep_transform_keys { |key| key.to_s.camelize(:lower) } if camelize
11
+
12
+ props_json = props.to_json
13
+ data = { react_component: name, react_props: props_json }
14
+
15
+ content = ""
16
+ if prerender
17
+ data[:react_prerender] = ""
18
+ content = Tombolo.renderer.render(name, props_json).html_safe # rubocop:disable Rails/OutputSafety
19
+ end
20
+
21
+ content_tag(:div, content, data:)
22
+ end
23
+ end
24
+ end
data/lib/tombolo.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tombolo/configuration"
4
+ require "tombolo/version"
5
+ require "tombolo/view_helper"
6
+ require "tombolo/railtie" if defined?(Rails::Railtie)
7
+
8
+ module Tombolo
9
+ class << self
10
+ def configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ def configure
15
+ yield configuration
16
+ end
17
+
18
+ def renderer
19
+ @renderer ||= begin
20
+ require "tombolo/renderer"
21
+ Renderer.new
22
+ end
23
+ end
24
+
25
+ def reset!
26
+ @configuration = nil
27
+ @renderer = nil
28
+ end
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tombolo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Inge Jørgensen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rails
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.0'
26
+ description: Drop-in replacement for react-rails with minimal footprint. Provides
27
+ a view helper and optional SSR via ExecJS.
28
+ email:
29
+ - inge@elektronaut.no
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/rails/generators/tombolo/install/install_generator.rb
38
+ - lib/rails/generators/tombolo/install/templates/initializer.rb
39
+ - lib/rails/generators/tombolo/install/templates/prerender.ts
40
+ - lib/tombolo.rb
41
+ - lib/tombolo/configuration.rb
42
+ - lib/tombolo/railtie.rb
43
+ - lib/tombolo/renderer.rb
44
+ - lib/tombolo/version.rb
45
+ - lib/tombolo/view_helper.rb
46
+ homepage: https://github.com/elektronaut/tombolo
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ rubygems_mfa_required: 'true'
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.2.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 4.0.3
66
+ specification_version: 4
67
+ summary: Minimal React component mounting for Rails
68
+ test_files: []