ruflet_rails 0.0.16 → 0.0.23
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/README.md +220 -28
- data/lib/generators/ruflet/install/install_generator.rb +3 -31
- data/lib/ruflet/rails/desktop_launcher.rb +2 -2
- data/lib/ruflet/rails/html_dsl/control_diff.rb +109 -0
- data/lib/ruflet/rails/html_dsl/html_app.rb +1328 -0
- data/lib/ruflet/rails/html_dsl/parser.rb +208 -0
- data/lib/ruflet/rails/html_dsl/styles.rb +793 -0
- data/lib/ruflet/rails/html_dsl/template_source.rb +292 -0
- data/lib/ruflet/rails/html_dsl/transformer.rb +1210 -0
- data/lib/ruflet/rails/html_dsl/view_helpers.rb +329 -0
- data/lib/ruflet/rails/html_dsl.rb +46 -0
- data/lib/ruflet/rails/install_support.rb +4 -20
- data/lib/ruflet/rails/native_app.rb +4 -2
- data/lib/ruflet/rails/native_screens.rb +92 -0
- data/lib/ruflet/rails/protocol.rb +0 -1
- data/lib/ruflet/rails/railtie.rb +4 -33
- data/lib/ruflet/rails/view_helpers.rb +6 -45
- data/lib/ruflet/rails.rb +10 -51
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +2 -1
- metadata +16 -11
- data/lib/ruflet/rails/protocol/web_app.rb +0 -243
- data/lib/ruflet/rails/protocol/web_socket_connection.rb +0 -126
- data/lib/ruflet/rails/protocol/wire_codec.rb +0 -268
- data/lib/ruflet/rails/web_installer.rb +0 -137
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ac0e488147c41963824bd806d53bd8d87ebdbf4b08cb616d8b802c441cbef45
|
|
4
|
+
data.tar.gz: e0196c7571b16596d187b0c5990f00ce9a72764aea7a82572d4922622eda3fb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59d33b4313bf7b40d66e7b3e03e88f788fd9d0db3690bb248400c67a706dad9d07002265e3f0e94ef1c49f0096a760d2f8cddfe3bb31c43cfcea62589b116f2c
|
|
7
|
+
data.tar.gz: cb32f2d8fa31c517e46dc38c829e1c3dd4d3cbdb504a6742d6a68ee36c7f4a6b893498a67bf18606331d3cc1e59ae201068f8b50f5ef66af8d069e728b7b53c7
|
data/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
`ruflet_rails` is the Rails-first integration package for Ruflet.
|
|
4
4
|
|
|
5
5
|
It mounts Ruby-driven Ruflet interfaces in a Rails application, makes Rails
|
|
6
|
-
views able to opt into native WebView chrome, and connects
|
|
7
|
-
|
|
6
|
+
views able to opt into native WebView chrome, and connects Ruflet clients to
|
|
7
|
+
the same application entrypoint.
|
|
8
8
|
|
|
9
9
|
## Add The Gem
|
|
10
10
|
|
|
@@ -17,18 +17,15 @@ gem "ruflet_rails"
|
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
bin/rails generate ruflet:install
|
|
20
|
-
bin/rails generate ruflet:install --web
|
|
21
20
|
bin/rails generate ruflet:install --desktop
|
|
22
|
-
bin/rails generate ruflet:install --web --desktop
|
|
23
21
|
```
|
|
24
22
|
|
|
25
23
|
This generator will:
|
|
26
24
|
- create `app/views/ruflet/main.rb`
|
|
27
25
|
- create `config/initializers/ruflet.rb`
|
|
28
26
|
- add the Ruflet WebSocket route to `config/routes.rb`
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
`--client=web|desktop|all` is used
|
|
27
|
+
- download the prebuilt desktop client when `--desktop` or
|
|
28
|
+
`--client=desktop` is used
|
|
32
29
|
|
|
33
30
|
Generated `config/initializers/ruflet.rb`:
|
|
34
31
|
|
|
@@ -48,24 +45,17 @@ At build time `ruflet_rails` serializes this Rails config into the Ruflet CLI
|
|
|
48
45
|
config shape, so the initializer remains the source of truth for app name,
|
|
49
46
|
backend URL, services, assets, and build colors.
|
|
50
47
|
|
|
51
|
-
##
|
|
48
|
+
## Connect a Ruflet client
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
bundle exec rake ruflet:web
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Mount the installed client and a developer-owned Ruflet entrypoint:
|
|
50
|
+
The generator adds an explicit WebSocket endpoint backed by the application
|
|
51
|
+
entrypoint:
|
|
61
52
|
|
|
62
53
|
```ruby
|
|
63
|
-
|
|
54
|
+
match "/ws", to: Ruflet::Rails.native(Rails.root.join("app/views/ruflet/main.rb")), via: :all
|
|
64
55
|
```
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
The
|
|
68
|
-
same `main.rb` also drives native clients through the generated `/ws` route.
|
|
57
|
+
Start Rails, then connect a Ruflet mobile or desktop client to that endpoint.
|
|
58
|
+
The Rails gem does not install or serve a second static web application.
|
|
69
59
|
|
|
70
60
|
## Build native clients from Rails
|
|
71
61
|
|
|
@@ -100,12 +90,11 @@ bin/rails server --desktop
|
|
|
100
90
|
bin/rails s --desktop
|
|
101
91
|
```
|
|
102
92
|
|
|
103
|
-
## Update prebuilt
|
|
93
|
+
## Update the prebuilt desktop client
|
|
104
94
|
|
|
105
|
-
|
|
95
|
+
Update the native desktop client:
|
|
106
96
|
|
|
107
97
|
```bash
|
|
108
|
-
bundle exec rake ruflet:web
|
|
109
98
|
bundle exec rake ruflet:update[desktop]
|
|
110
99
|
```
|
|
111
100
|
|
|
@@ -120,11 +109,214 @@ bundle exec rake ruflet:install
|
|
|
120
109
|
bundle exec rake ruflet:install[DEVICE_ID]
|
|
121
110
|
```
|
|
122
111
|
|
|
112
|
+
## HTML as the UI DSL (no WebView)
|
|
113
|
+
|
|
114
|
+
Beyond the WebView shell, Ruflet can treat HTML itself as the UI language:
|
|
115
|
+
Rails views describe screens with markup, and `Ruflet::Rails.erb_to_native`
|
|
116
|
+
compiles each page into **real native Ruflet controls** — no WebView anywhere.
|
|
117
|
+
This is HTML-over-the-wire for native UI: state lives in Rails, every
|
|
118
|
+
interaction is a request, and the response markup re-renders the screen.
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# app/views/ruflet/main.rb
|
|
122
|
+
Ruflet.run do |page|
|
|
123
|
+
Ruflet::Rails.erb_to_native(page, start_url: "/native")
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Screens are ordinary Rails views (ERB, controllers, sessions, and redirects
|
|
128
|
+
all work — requests carry the Rails session cookie and CSRF token, plus an
|
|
129
|
+
`X-Ruflet-Native: 1` header so a controller can render native markup and a
|
|
130
|
+
browser page from the same action):
|
|
131
|
+
|
|
132
|
+
```erb
|
|
133
|
+
<%# app/views/counters/show.html.erb %>
|
|
134
|
+
<appbar title="Counter"></appbar>
|
|
135
|
+
|
|
136
|
+
<column class="p-6 gap-4 items-center justify-center flex-1">
|
|
137
|
+
<text class="text-5xl font-bold text-slate-900"><%= @count %></text>
|
|
138
|
+
<row class="gap-3">
|
|
139
|
+
<button variant="outlined" icon="remove" on-click="<%= counter_decrement_path %>">Down</button>
|
|
140
|
+
<button variant="filled" icon="add" on-click="<%= counter_increment_path %>">Up</button>
|
|
141
|
+
</row>
|
|
142
|
+
<a href="<%= settings_path %>">Settings</a>
|
|
143
|
+
</column>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- **Layout** — `<column>`, `<row>`, `<stack>`, `<div>`/`<section>` (container),
|
|
147
|
+
`<card>`, `<center>`, `<spacer>`, `<list>`, `<grid>`.
|
|
148
|
+
- **Content** — `<text>`, `<h1>`–`<h6>`, `<p>`, `<markdown>`, `<img>`, `<icon>`,
|
|
149
|
+
`<hr>`, `<ul>`/`<li>`.
|
|
150
|
+
- **Styling** — a broad Tailwind-flavored `class` vocabulary that maps onto
|
|
151
|
+
each control's real props (only the ones a control accepts are applied):
|
|
152
|
+
- spacing & size: `p-4`, `px-6`, `m-2`, `-mt-2`, `gap-3`, `w-64`, `h-full`,
|
|
153
|
+
`flex-1`, `aspect-video`, `aspect-[4/3]`
|
|
154
|
+
- color: `bg-slate-100`, `text-emerald-600`, `bg-[#123456]`, theme tokens
|
|
155
|
+
(`bg-primary`, `text-on-surface`), gradients
|
|
156
|
+
(`bg-gradient-to-r from-blue-500 via-sky-400 to-cyan-300`)
|
|
157
|
+
- typography: `text-xl`, `text-[42]`, `font-bold`, `font-mono`, `italic`,
|
|
158
|
+
`text-center`, `tracking-wide`, `leading-relaxed`, `underline`,
|
|
159
|
+
`line-through`, `uppercase`/`lowercase`/`capitalize`, `truncate`,
|
|
160
|
+
`line-clamp-2`, `whitespace-nowrap`, `select-none`, `text-ellipsis`
|
|
161
|
+
- shape & borders: `rounded-2xl`, `rounded-t-lg`, `rounded-br-sm`, `border`,
|
|
162
|
+
`border-2`, `border-t`, `border-red-500`, `shadow-lg`, `opacity-75`, `blur-md`
|
|
163
|
+
- transforms & position: `rotate-45`, `-rotate-12`, `scale-95`, `top-4`,
|
|
164
|
+
`left-2`, `inset-0`/`inset-x-4` (Stack children)
|
|
165
|
+
- transitions: `transition`, `duration-300`, `ease-in-out` (implicit animation)
|
|
166
|
+
- layout & display: `items-center`, `justify-between`, `place-center`,
|
|
167
|
+
`flex-wrap`, `gap-x-3`/`gap-y-2`, `size-12`, `w-screen`, `min-h-screen`,
|
|
168
|
+
`scroll`, `overflow-hidden`, `object-cover` (image fit), `hidden`/`invisible`
|
|
169
|
+
Anything not mapped is ignored; the full native prop is always available as
|
|
170
|
+
an explicit attribute (`gradient='{…}'`, `blur="8"`, …).
|
|
171
|
+
- **Navigation** — `<a href>` pushes a native screen; `nav="replace|root|back"`
|
|
172
|
+
change the mode. The native back button/gesture pops.
|
|
173
|
+
- **Actions** — `on-click="/counter/increment"` posts to Rails and re-renders
|
|
174
|
+
the current screen in place with the response (`redirect_to` is followed).
|
|
175
|
+
Prefix a verb for other methods: `on-click="delete:/items/3"`.
|
|
176
|
+
- **Forms** — `<form action method>` with named `<input>`, `<textarea>`,
|
|
177
|
+
`<select>` fields tracks values natively and submits them like a normal
|
|
178
|
+
Rails form, then renders the response.
|
|
179
|
+
- **App chrome** — `<appbar title="Inbox" leading-icon="menu">` with
|
|
180
|
+
`<action icon="search" href="/search"/>` children becomes the native AppBar.
|
|
181
|
+
- **The whole widget catalog** — any other tag falls through to the ruflet_core
|
|
182
|
+
control registry with its attributes as props:
|
|
183
|
+
`<progress-bar value="0.4">`, `<switch label="Dark mode">`,
|
|
184
|
+
`<chip label="New">`… kebab-case maps to the control name.
|
|
185
|
+
|
|
186
|
+
**First-class components** — these have dedicated tags/helpers that map to the
|
|
187
|
+
right native shape (some Ruflet "components" are *props* on another control,
|
|
188
|
+
not controls of their own — the DSL handles that for you):
|
|
189
|
+
|
|
190
|
+
| Tag | Helper | Notes |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `<badge label="3">…</badge>` | `badge` | wraps its child; badge is a prop on it |
|
|
193
|
+
| `<tooltip message="…">…</tooltip>` | `tooltip` | prop on its child |
|
|
194
|
+
| `<chip label="Ruby" icon="star">` | `chip` | text → `label`; `on-click`/`href` supported |
|
|
195
|
+
| `<avatar src>` / `<avatar>AM</avatar>` | `avatar` | image or initials |
|
|
196
|
+
| `<list-tile title subtitle leading href>` | `list_tile` | taps navigate/act natively |
|
|
197
|
+
| `<expansion-tile title>…</expansion-tile>` | `expansion_tile` | collapsible section |
|
|
198
|
+
| `<switch>` `<checkbox>` `<slider>` `<radio>` | same | standalone; `name` makes them form fields |
|
|
199
|
+
| `<radio-group name value>…radios…</radio-group>` | `radio_group` | |
|
|
200
|
+
| `<segmented-button name value>…<segment>…</segmented-button>` | `segmented_button` | |
|
|
201
|
+
| `<tabs><tab label icon>…</tab></tabs>` | `tabs` / `tab` | native TabBar + panes |
|
|
202
|
+
| `<table><thead><tr><th>…` | — | plain HTML tables → native `DataTable` |
|
|
203
|
+
| `<fab icon href>` | `fab` | mounts as the screen's FloatingActionButton |
|
|
204
|
+
| `<bottom-nav><nav-item icon label href selected>…` | `bottom_nav` / `nav_item` | screen's NavigationBar; a tab resets to that URL as root |
|
|
205
|
+
|
|
206
|
+
**Every Ruflet control is reachable.** Any tag without a dedicated builder
|
|
207
|
+
maps straight onto the ruflet_core control registry — the entire catalog
|
|
208
|
+
(charts, canvas, cupertino_*, maps, sensors, list/menu/expansion controls, …)
|
|
209
|
+
works from markup. The passthrough is schema-aware: a control's text and child
|
|
210
|
+
elements are routed into whichever prop it actually accepts (`content`,
|
|
211
|
+
`controls`, `label`, `title`), so `<filled-button>Save</filled-button>`,
|
|
212
|
+
`<banner><text>…</text></banner>`, and `<responsive-row>…</responsive-row>`
|
|
213
|
+
all build correctly. Use the raw tag (kebab-case → control name) or the
|
|
214
|
+
`widget` helper:
|
|
215
|
+
|
|
216
|
+
```erb
|
|
217
|
+
<%= widget "bar-chart", expand: true do %>
|
|
218
|
+
<%= widget "bar-chart-group", x: 0 do %>
|
|
219
|
+
<%= widget "bar-chart-rod", from_y: 0, to_y: 10 %>
|
|
220
|
+
<% end %>
|
|
221
|
+
<% end %>
|
|
222
|
+
|
|
223
|
+
<cupertino-activity-indicator animating="true"></cupertino-activity-indicator>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Services and extensions.** Non-visual platform services (`<geolocator>`,
|
|
227
|
+
`<battery>`, `<clipboard>`, `<flashlight>`, `<permission-handler>`,
|
|
228
|
+
`<secure-storage>`, `<shared-preferences>`, `<file-picker>`, `<wakelock>`,
|
|
229
|
+
`<share>`, `<url-launcher>`, the sensors, …) are non-visual: declaring one
|
|
230
|
+
mounts it on the screen's service registry rather than in the layout. Extension
|
|
231
|
+
controls that *do* render (`<camera>`, `<audio>`, `<video>`, `<lottie>`,
|
|
232
|
+
`<rive>`, `<map>`, `<code-editor>`, `<web-view>`, `<spinkit>`,
|
|
233
|
+
`<color-picker>`, the charts) render inline like any other control. Each has a
|
|
234
|
+
matching helper:
|
|
235
|
+
|
|
236
|
+
```erb
|
|
237
|
+
<%= geolocator %>
|
|
238
|
+
<%= camera id: "camera-preview", preview_enabled: true %>
|
|
239
|
+
<%= video src: "clip.mp4" %>
|
|
240
|
+
<%= lottie src: "loader.json" %>
|
|
241
|
+
<%= map do %><%= widget "tile-layer", url_template: "…" %><% end %>
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Buttons can invoke native services without a request back to Rails. Common
|
|
245
|
+
operations have short names, and the open-ended `method`/`args` form exposes
|
|
246
|
+
new service methods without waiting for another `ruflet_rails` release:
|
|
247
|
+
|
|
248
|
+
```erb
|
|
249
|
+
<%= button "Copy", service: "copy", text: "Hello from Rails" %>
|
|
250
|
+
<%= button "Locate", service: "location" %>
|
|
251
|
+
<%= button "Allow camera",
|
|
252
|
+
service: "permission-handler",
|
|
253
|
+
method: "request",
|
|
254
|
+
args: { permission: "camera" } %>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
An inline extension can also be controlled by its native ID:
|
|
258
|
+
|
|
259
|
+
```erb
|
|
260
|
+
<%= audio id: "player", src: "https://example.com/song.mp3" %>
|
|
261
|
+
<%= button "Play", service: "control", target: "player", method: "play" %>
|
|
262
|
+
<%= button "Seek", service: "control", target: "player",
|
|
263
|
+
method: "seek", position: 2_000 %>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Service results and errors are reported in a native dialog by default. For a
|
|
267
|
+
stream such as an accelerometer, pass `result_target: "status-control-id"` to
|
|
268
|
+
update an inline Text control instead.
|
|
269
|
+
|
|
270
|
+
```erb
|
|
271
|
+
<%# a form, natively rendered %>
|
|
272
|
+
<form action="<%= session_path %>" method="post">
|
|
273
|
+
<column class="p-6 gap-4">
|
|
274
|
+
<h2>Sign in</h2>
|
|
275
|
+
<input type="email" name="email" label="Email" placeholder="you@example.com">
|
|
276
|
+
<input type="password" name="password" label="Password">
|
|
277
|
+
<input type="checkbox" name="remember" label="Remember me" checked>
|
|
278
|
+
<input type="submit" value="Sign in">
|
|
279
|
+
</column>
|
|
280
|
+
</form>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Ruby helper DSL
|
|
284
|
+
|
|
285
|
+
The same markup can be authored with Ruby helpers (auto-included into
|
|
286
|
+
ActionView), and both styles mix freely in one template. Attribute keys are
|
|
287
|
+
snake_case and become kebab-case attributes (`on_click:` → `on-click`);
|
|
288
|
+
Hash/Array values serialize as JSON:
|
|
289
|
+
|
|
290
|
+
```erb
|
|
291
|
+
<%= appbar "Counter" %>
|
|
292
|
+
|
|
293
|
+
<%= column class: "p-6 gap-6 items-center justify-center flex-1" do %>
|
|
294
|
+
<%= text @count, class: "text-[96] font-bold" %>
|
|
295
|
+
<%= row class: "gap-3" do %>
|
|
296
|
+
<%= button "Down", variant: "outlined", icon: "remove",
|
|
297
|
+
on_click: counter_decrement_path %>
|
|
298
|
+
<%= button "Up", variant: "filled", icon: "add",
|
|
299
|
+
on_click: counter_increment_path %>
|
|
300
|
+
<% end %>
|
|
301
|
+
<%= link "Settings", settings_path %>
|
|
302
|
+
<% end %>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Available helpers: layout (`column`, `row`, `stack`, `card`, `center`,
|
|
306
|
+
`list`, `grid`, `spacer`, `divider`), content (`text`, `h1`–`h6`/`heading`,
|
|
307
|
+
`markdown`, `image`, `icon`), interaction (`button`, `link`), chrome
|
|
308
|
+
(`appbar`, `appbar_action`), forms (`form`, `input`, `textarea`, `dropdown`,
|
|
309
|
+
`submit`), and `widget("progress-bar", value: 0.4)` for anything else in the
|
|
310
|
+
control registry.
|
|
311
|
+
|
|
312
|
+
Pick the mode per app: `erb_to_native` when HTML should *become* native controls,
|
|
313
|
+
`native_shell` when you want the real web page in a WebView with native chrome.
|
|
314
|
+
|
|
123
315
|
## Native WebView shell
|
|
124
316
|
|
|
125
317
|
Beyond the server-driven UI, Ruflet can wrap an existing Rails HTML app in a
|
|
126
318
|
managed native WebView. This is an opt-in shell: calling
|
|
127
|
-
`Ruflet::Rails.
|
|
319
|
+
`Ruflet::Rails.native_shell` wraps web pages in a native Ruflet shell whose body is
|
|
128
320
|
a WebView. Plain `Ruflet.run { |page| ... }` remains a normal Ruflet app with no
|
|
129
321
|
WebView wrapper or HTML adapter.
|
|
130
322
|
|
|
@@ -136,7 +328,7 @@ promote page chrome.
|
|
|
136
328
|
```ruby
|
|
137
329
|
# app/views/ruflet/main.rb
|
|
138
330
|
Ruflet.run do |page|
|
|
139
|
-
Ruflet::Rails.
|
|
331
|
+
Ruflet::Rails.native_shell(
|
|
140
332
|
page,
|
|
141
333
|
start_url: "https://myapp.com",
|
|
142
334
|
title: "My App", # opt into a native AppBar (tracks <title>)
|
|
@@ -290,13 +482,13 @@ Add `data-ruflet-close="false"` when a sheet link should not dismiss:
|
|
|
290
482
|
<%= ruflet_haptic_button "Tap", style: "light" %>
|
|
291
483
|
```
|
|
292
484
|
|
|
293
|
-
For a Fizzy-style web app, keep `Ruflet::Rails.
|
|
485
|
+
For a Fizzy-style web app, keep `Ruflet::Rails.native_shell` simple and opt in from
|
|
294
486
|
the views that need native treatment:
|
|
295
487
|
|
|
296
488
|
```ruby
|
|
297
489
|
# app/views/ruflet/main.rb
|
|
298
490
|
Ruflet.run do |page|
|
|
299
|
-
Ruflet::Rails.
|
|
491
|
+
Ruflet::Rails.native_shell(page, start_url: "#{Ruflet::Rails.backend_url}/")
|
|
300
492
|
end
|
|
301
493
|
```
|
|
302
494
|
|
|
@@ -315,7 +507,7 @@ end
|
|
|
315
507
|
Mount it in Rails:
|
|
316
508
|
|
|
317
509
|
```ruby
|
|
318
|
-
match "/ws", to: Ruflet::Rails.
|
|
510
|
+
match "/ws", to: Ruflet::Rails.native(Rails.root.join("app/views/ruflet/main.rb")), via: :all
|
|
319
511
|
```
|
|
320
512
|
|
|
321
513
|
The same mounted Ruby entrypoint drives mobile, web, and desktop clients.
|
|
@@ -6,9 +6,8 @@ require "ruflet/rails/install_support"
|
|
|
6
6
|
module Ruflet
|
|
7
7
|
module Generators
|
|
8
8
|
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
-
class_option :web, type: :boolean, default: false, desc: "Install the prebuilt Ruflet web client"
|
|
10
9
|
class_option :desktop, type: :boolean, default: false, desc: "Download the server-driven desktop Ruflet client"
|
|
11
|
-
class_option :client, type: :string, default: nil, desc: "Install prebuilt clients:
|
|
10
|
+
class_option :client, type: :string, default: nil, desc: "Install prebuilt clients: desktop or none"
|
|
12
11
|
|
|
13
12
|
desc "Install Ruflet into a Rails app."
|
|
14
13
|
|
|
@@ -31,21 +30,11 @@ module Ruflet
|
|
|
31
30
|
def mount_websocket
|
|
32
31
|
routes = File.join(destination_root, "config", "routes.rb")
|
|
33
32
|
return unless File.file?(routes)
|
|
34
|
-
return if File.read(routes).include?("Ruflet::Rails.
|
|
33
|
+
return if File.read(routes).include?("Ruflet::Rails.native(")
|
|
35
34
|
|
|
36
35
|
route Ruflet::Rails::InstallSupport.route_snippet(entrypoint: entrypoint_path)
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
def mount_web_app
|
|
40
|
-
return unless web_requested?
|
|
41
|
-
|
|
42
|
-
routes = File.join(destination_root, "config", "routes.rb")
|
|
43
|
-
return unless File.file?(routes)
|
|
44
|
-
return if File.read(routes).include?("Ruflet::Rails.web_app(")
|
|
45
|
-
|
|
46
|
-
route Ruflet::Rails::InstallSupport.web_route_snippet(entrypoint: entrypoint_path)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
38
|
def add_desktop_flag_to_binstubs
|
|
50
39
|
return unless desktop_requested?
|
|
51
40
|
|
|
@@ -57,7 +46,6 @@ module Ruflet
|
|
|
57
46
|
client = requested_client
|
|
58
47
|
return if client == "none"
|
|
59
48
|
|
|
60
|
-
install_web_client if %w[web all].include?(client)
|
|
61
49
|
install_desktop_client if %w[desktop all].include?(client)
|
|
62
50
|
rescue StandardError => e
|
|
63
51
|
@client_download_failed = true
|
|
@@ -66,7 +54,6 @@ module Ruflet
|
|
|
66
54
|
|
|
67
55
|
def print_install_status
|
|
68
56
|
Ruflet::Rails::InstallSupport.install_next_steps(
|
|
69
|
-
target: install_target,
|
|
70
57
|
entrypoint: entrypoint_path,
|
|
71
58
|
client: requested_client
|
|
72
59
|
).each { |line| say line }
|
|
@@ -85,13 +72,11 @@ module Ruflet
|
|
|
85
72
|
def requested_client
|
|
86
73
|
explicit = options[:client].to_s.strip.downcase
|
|
87
74
|
unless explicit.empty?
|
|
88
|
-
raise Thor::Error, "--client must be
|
|
75
|
+
raise Thor::Error, "--client must be desktop or none" unless %w[desktop none].include?(explicit)
|
|
89
76
|
|
|
90
77
|
return explicit
|
|
91
78
|
end
|
|
92
79
|
|
|
93
|
-
return "all" if options[:web] && options[:desktop]
|
|
94
|
-
return "web" if options[:web]
|
|
95
80
|
return "desktop" if options[:desktop]
|
|
96
81
|
|
|
97
82
|
"none"
|
|
@@ -101,19 +86,6 @@ module Ruflet
|
|
|
101
86
|
%w[desktop all].include?(requested_client)
|
|
102
87
|
end
|
|
103
88
|
|
|
104
|
-
def web_requested?
|
|
105
|
-
%w[web all].include?(requested_client)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def install_target
|
|
109
|
-
"ruflet"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def install_web_client
|
|
113
|
-
return if Ruflet::Rails::WebInstaller.install!(root: destination_root)
|
|
114
|
-
|
|
115
|
-
client_download_failed("web")
|
|
116
|
-
end
|
|
117
89
|
|
|
118
90
|
def install_desktop_client
|
|
119
91
|
require "ruflet/cli"
|
|
@@ -8,7 +8,7 @@ module Ruflet
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
def launch_once(root:, argv: ARGV, env: ENV, wait: 1.0)
|
|
11
|
-
return false unless desktop_enabled?(
|
|
11
|
+
return false unless desktop_enabled?(argv: argv, env: env)
|
|
12
12
|
return false unless env["RUFLET_RAILS_DESKTOP_SERVER"].to_s.downcase == "true" || rails_server_command?(argv)
|
|
13
13
|
return false if env["RUFLET_RAILS_DESKTOP"].to_s.downcase == "false"
|
|
14
14
|
return false if launched?
|
|
@@ -51,7 +51,7 @@ module Ruflet
|
|
|
51
51
|
"http://localhost:#{env["PORT"].to_s.empty? ? 3000 : env["PORT"]}"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
def desktop_enabled?(
|
|
54
|
+
def desktop_enabled?(argv: ARGV, env: ENV)
|
|
55
55
|
return true if env["RUFLET_RAILS_DESKTOP_SERVER"].to_s.downcase == "true"
|
|
56
56
|
return true if env["RUFLET_RAILS_DESKTOP"].to_s.downcase == "true"
|
|
57
57
|
return true if Array(argv).map(&:to_s).include?("--desktop")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ruflet
|
|
4
|
+
module Rails
|
|
5
|
+
module HtmlDsl
|
|
6
|
+
# Compares the control tree already mounted on a screen with the one just
|
|
7
|
+
# built from fresh markup, and reports the minimum set of prop updates
|
|
8
|
+
# that turns the first into the second.
|
|
9
|
+
#
|
|
10
|
+
# Re-rendering a screen in place otherwise means shipping every control
|
|
11
|
+
# on it to change one value — a counter tick re-sends the heading, both
|
|
12
|
+
# buttons and the caption to move a single digit, and the client rebuilds
|
|
13
|
+
# that subtree (losing scroll position and any transient widget state).
|
|
14
|
+
#
|
|
15
|
+
# The diff is deliberately conservative: it only reports updates when the
|
|
16
|
+
# two trees have exactly the same shape (same control types, same number
|
|
17
|
+
# of children, same prop-held controls, in the same order). Any structural
|
|
18
|
+
# difference returns nil, and the caller replaces the body wholesale as
|
|
19
|
+
# before. Shape changes are where a diff would be subtly wrong, and they
|
|
20
|
+
# are also the case where a full replace costs what it should.
|
|
21
|
+
class ControlDiff
|
|
22
|
+
# Props whose values are server-side plumbing, never wire state.
|
|
23
|
+
SKIPPED_PROPS = %w[key].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(old_tree, new_tree)
|
|
26
|
+
@old_tree = old_tree
|
|
27
|
+
@new_tree = new_tree
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# [[mounted_control, {prop => value}], …], or nil when the shape moved.
|
|
31
|
+
def updates
|
|
32
|
+
@updates = []
|
|
33
|
+
return nil unless walk(@old_tree, @new_tree)
|
|
34
|
+
|
|
35
|
+
@updates
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def walk(old_node, new_node)
|
|
41
|
+
return true if old_node.nil? && new_node.nil?
|
|
42
|
+
return false if old_node.nil? || new_node.nil?
|
|
43
|
+
|
|
44
|
+
if old_node.is_a?(Array) || new_node.is_a?(Array)
|
|
45
|
+
return false unless old_node.is_a?(Array) && new_node.is_a?(Array)
|
|
46
|
+
return false unless old_node.length == new_node.length
|
|
47
|
+
|
|
48
|
+
return old_node.each_with_index.all? { |item, i| walk(item, new_node[i]) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
return old_node == new_node unless control?(old_node) || control?(new_node)
|
|
52
|
+
return false unless control?(old_node) && control?(new_node)
|
|
53
|
+
return false unless old_node.type == new_node.type
|
|
54
|
+
return false unless old_node.children.length == new_node.children.length
|
|
55
|
+
|
|
56
|
+
return false unless compare_props(old_node, new_node)
|
|
57
|
+
|
|
58
|
+
old_node.children.each_with_index.all? { |child, i| walk(child, new_node.children[i]) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def compare_props(old_node, new_node)
|
|
62
|
+
keys = (old_node.props.keys | new_node.props.keys) - SKIPPED_PROPS
|
|
63
|
+
changed = {}
|
|
64
|
+
|
|
65
|
+
keys.each do |key|
|
|
66
|
+
old_value = old_node.props[key]
|
|
67
|
+
new_value = new_node.props[key]
|
|
68
|
+
|
|
69
|
+
# A prop holding controls is structure: recurse instead of comparing.
|
|
70
|
+
if holds_controls?(old_value) || holds_controls?(new_value)
|
|
71
|
+
return false unless walk(old_value, new_value)
|
|
72
|
+
|
|
73
|
+
next
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
changed[key.to_sym] = new_value unless old_value == new_value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The fresh tree carries handlers bound to this render (a button's
|
|
80
|
+
# on-click closes over the screen it was built for). Move them onto
|
|
81
|
+
# the mounted control so taps keep working after a patch; handlers
|
|
82
|
+
# live server-side, so this costs nothing on the wire.
|
|
83
|
+
rebind_handlers(old_node, new_node)
|
|
84
|
+
|
|
85
|
+
@updates << [old_node, changed] unless changed.empty?
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def rebind_handlers(old_node, new_node)
|
|
90
|
+
handlers = new_node.instance_variable_get(:@handlers)
|
|
91
|
+
return if handlers.nil? || handlers.empty?
|
|
92
|
+
|
|
93
|
+
old_node.instance_variable_set(:@handlers, handlers)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def control?(node)
|
|
97
|
+
node.is_a?(Ruflet::Control)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def holds_controls?(value)
|
|
101
|
+
return true if control?(value)
|
|
102
|
+
return value.any? { |item| control?(item) } if value.is_a?(Array)
|
|
103
|
+
|
|
104
|
+
false
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|