ruflet_rails 0.0.7 → 0.0.8
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 +152 -5
- data/lib/generators/ruflet/form/form_generator.rb +55 -0
- data/lib/generators/ruflet/install/install_generator.rb +111 -22
- data/lib/generators/ruflet/scaffold/scaffold_generator.rb +60 -0
- data/lib/ruflet/rails/desktop_launcher.rb +116 -0
- data/lib/ruflet/rails/form_helpers.rb +161 -0
- data/lib/ruflet/rails/install_support.rb +911 -133
- data/lib/ruflet/rails/protocol/endpoint.rb +21 -5
- data/lib/ruflet/rails/protocol/local_server.rb +55 -12
- data/lib/ruflet/rails/protocol/runner.rb +22 -9
- data/lib/ruflet/rails/protocol/web_socket_connection.rb +3 -118
- data/lib/ruflet/rails/protocol/wire_codec.rb +3 -243
- data/lib/ruflet/rails/railtie.rb +63 -4
- data/lib/ruflet/rails/resource_component.rb +191 -0
- data/lib/ruflet/rails/resource_view.rb +124 -0
- data/lib/ruflet/rails/session_registry.rb +94 -0
- data/lib/ruflet/rails/view.rb +57 -0
- data/lib/ruflet/rails.rb +167 -2
- data/lib/ruflet/version.rb +1 -1
- data/lib/ruflet_rails.rb +8 -1
- metadata +13 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aefcbfd0f43a7987f825223bcb14311816dc94a4e81039d6d955d504b51f01ae
|
|
4
|
+
data.tar.gz: d274aedaf03e16b8298b2fabd68a3dbd14aea153ac0abf0664a5198244aaf4df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d8fb4d2f5dfc8ab871999d0aed10672fc164790a13c4a9286ef079ff4f7777820b01e01188d3fcc605919d60e8ee79e8a085805c03ee5446805ab92d2f7dc56
|
|
7
|
+
data.tar.gz: 061b1e5641f03da87372ed31f47b841c9c9ab0f16a0cbd05639f8bfbf7aec5248879f2b8b608fb388c68f0dbe599976a2cfd1271005cf7883570fd58f511e5f8
|
data/README.md
CHANGED
|
@@ -16,20 +16,24 @@ gem "ruflet_rails", ">= 0.0.5"
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
bin/rails generate ruflet:install
|
|
19
|
+
bin/rails generate ruflet:install --web
|
|
20
|
+
bin/rails generate ruflet:install --desktop
|
|
21
|
+
bin/rails generate ruflet:install --web --desktop
|
|
19
22
|
```
|
|
20
23
|
|
|
21
24
|
This generator will:
|
|
22
|
-
- create `app/
|
|
25
|
+
- create `app/views/ruflet/main.rb`
|
|
23
26
|
- create `ruflet.yaml`
|
|
24
27
|
- add the Ruflet mount route to `config/routes.rb`
|
|
25
|
-
-
|
|
28
|
+
- download prebuilt clients from GitHub releases when `--web`, `--desktop`, or
|
|
29
|
+
`--client=web|desktop|all` is used
|
|
26
30
|
|
|
27
31
|
Generated `ruflet.yaml`:
|
|
28
32
|
|
|
29
33
|
```yaml
|
|
30
34
|
app:
|
|
31
35
|
name: My App
|
|
32
|
-
|
|
36
|
+
backend_url: http://localhost:3000
|
|
33
37
|
|
|
34
38
|
services: []
|
|
35
39
|
|
|
@@ -55,10 +59,147 @@ bundle exec rake ruflet:build[ios]
|
|
|
55
59
|
bundle exec rake ruflet:build[aab]
|
|
56
60
|
```
|
|
57
61
|
|
|
62
|
+
Rails web builds are published to `public/ruflet` and served by Rails at `/ruflet/`.
|
|
63
|
+
|
|
64
|
+
`desktop` is also accepted as a host-platform alias:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
bundle exec rake ruflet:build[desktop]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Rails desktop builds are server-driven. The built desktop app connects back to the
|
|
71
|
+
Rails backend configured in `ruflet.yaml`; it does not package a self-contained
|
|
72
|
+
Ruby runtime.
|
|
73
|
+
|
|
74
|
+
Plain Rails dev server commands do not launch the desktop app. Request a desktop
|
|
75
|
+
client explicitly with a flag:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bin/dev --desktop
|
|
79
|
+
bin/rails server --desktop
|
|
80
|
+
bin/rails s --desktop
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Update prebuilt clients
|
|
84
|
+
|
|
85
|
+
Uses the same GitHub release assets as `ruflet update`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
bundle exec rake ruflet:update[web]
|
|
89
|
+
bundle exec rake ruflet:update[desktop]
|
|
90
|
+
bundle exec rake ruflet:update[all]
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For web, the downloaded static client is published to `public/ruflet` and served
|
|
94
|
+
by Rails at `/ruflet/`. The Rails app does not vendor Flutter source code.
|
|
95
|
+
|
|
96
|
+
## Install mobile build
|
|
97
|
+
|
|
98
|
+
Uses the same install pipeline as `ruflet install`:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
bundle exec rake ruflet:install
|
|
102
|
+
bundle exec rake ruflet:install[DEVICE_ID]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Ruflet resource scaffolds
|
|
106
|
+
|
|
107
|
+
Generate a Ruflet CRUD view for an existing Rails model:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
bin/rails generate ruflet:scaffold Post
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The scaffold creates generated app code the Rails developer can own and edit:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
# app/views/ruflet/posts_view.rb
|
|
117
|
+
require_relative "components/posts/post_component"
|
|
118
|
+
|
|
119
|
+
class PostView < RufletView
|
|
120
|
+
include Ruflet::Rails::FormHelpers
|
|
121
|
+
|
|
122
|
+
route "/posts"
|
|
123
|
+
|
|
124
|
+
def render
|
|
125
|
+
page.title = resource_title
|
|
126
|
+
render_index
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def records
|
|
132
|
+
# edit resource query logic here
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def component
|
|
136
|
+
@component ||= PostComponent.new(page, controller: self)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# app/views/ruflet/components/posts/post_component.rb
|
|
141
|
+
class PostComponent < Ruflet::Rails::ResourceComponent
|
|
142
|
+
def render
|
|
143
|
+
# edit the scaffold layout here
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The generated view contains the resource logic: query, model helpers, field
|
|
149
|
+
introspection, save/delete, dialog close, and display formatting. The generated
|
|
150
|
+
component contains the UI: tables, lists, and dialogs. `ruflet_rails` only
|
|
151
|
+
provides the base classes and generic helpers. The generator does not dump field
|
|
152
|
+
declarations like `title:string` or literal metadata tables into the app.
|
|
153
|
+
|
|
154
|
+
## Ruflet model forms
|
|
155
|
+
|
|
156
|
+
Generate only a reusable Ruflet form for an existing Rails model:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
bin/rails generate ruflet:form Post
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
When no fields are passed, the generator reads the model columns and skips `id`,
|
|
163
|
+
`created_at`, and `updated_at`. You can also pass fields explicitly:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
bin/rails generate ruflet:form Post title:string body:text published:boolean category:references
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Foreign keys and references, such as `category:references` or `user_id`, render
|
|
170
|
+
as Ruflet dropdowns populated from the associated Rails model.
|
|
171
|
+
|
|
172
|
+
The generated form lives at `app/views/ruflet/components/posts/post_form.rb`.
|
|
173
|
+
The form generator creates `app/views/ruflet/components/application_component.rb`
|
|
174
|
+
when that base component does not already exist.
|
|
175
|
+
|
|
176
|
+
## Shared Ruflet components
|
|
177
|
+
|
|
178
|
+
Put shared Ruflet UI components under `app/views/ruflet/components`. Component
|
|
179
|
+
files are loaded before `*_view.rb` files, so views can call them directly:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
# app/views/ruflet/components/page_title_component.rb
|
|
183
|
+
class PageTitleComponent < ApplicationComponent
|
|
184
|
+
def render(value)
|
|
185
|
+
text(value, size: desktop? || web? ? 28 : 24, weight: "bold")
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
# app/views/ruflet/posts/posts_view.rb
|
|
192
|
+
class PostsView < RufletView
|
|
193
|
+
def render
|
|
194
|
+
page.add(PageTitleComponent.render(page, "Posts"))
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
58
199
|
## Manual usage
|
|
59
200
|
|
|
60
201
|
```ruby
|
|
61
|
-
# app/
|
|
202
|
+
# app/views/ruflet/main.rb
|
|
62
203
|
require "ruflet"
|
|
63
204
|
|
|
64
205
|
Ruflet.run do |page|
|
|
@@ -67,4 +208,10 @@ Ruflet.run do |page|
|
|
|
67
208
|
end
|
|
68
209
|
```
|
|
69
210
|
|
|
70
|
-
|
|
211
|
+
Mount it in Rails:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
match "/ws", to: Ruflet::Rails.app(Rails.root.join("app/views/ruflet/main.rb")), via: :all
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The same mounted Ruby entrypoint drives mobile, web, and desktop clients.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
require "ruflet/rails/install_support"
|
|
6
|
+
|
|
7
|
+
module Ruflet
|
|
8
|
+
module Generators
|
|
9
|
+
class FormGenerator < ::Rails::Generators::Base
|
|
10
|
+
argument :model_name, type: :string
|
|
11
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
12
|
+
|
|
13
|
+
desc "Generate only a Ruflet form for an existing Rails model."
|
|
14
|
+
|
|
15
|
+
def create_application_component
|
|
16
|
+
target = File.join(destination_root, Ruflet::Rails::InstallSupport.application_component_path)
|
|
17
|
+
return if File.exist?(target)
|
|
18
|
+
|
|
19
|
+
create_file target, Ruflet::Rails::InstallSupport.application_component_template
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create_ruflet_form
|
|
23
|
+
target = File.join(destination_root, form_view_path)
|
|
24
|
+
|
|
25
|
+
create_file(
|
|
26
|
+
target,
|
|
27
|
+
Ruflet::Rails::InstallSupport.form_view_template(
|
|
28
|
+
model_name: model_name,
|
|
29
|
+
attributes: form_attributes
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def print_form_status
|
|
35
|
+
names = Ruflet::Rails::InstallSupport.model_names(model_name)
|
|
36
|
+
say "Ruflet form generated at #{form_view_path}"
|
|
37
|
+
say "Call #{names[:class_name]}Form.render(page, record: #{names[:class_name]}.new) from any Ruflet view."
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def form_view_path
|
|
43
|
+
Ruflet::Rails::InstallSupport.form_view_path(model_name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def form_attributes
|
|
47
|
+
return attributes unless attributes.empty?
|
|
48
|
+
|
|
49
|
+
model_class = model_name.to_s.camelize.safe_constantize
|
|
50
|
+
inferred = Ruflet::Rails::InstallSupport.attributes_from_model(model_class)
|
|
51
|
+
inferred.empty? ? attributes : inferred
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -6,50 +6,88 @@ require "ruflet/rails/install_support"
|
|
|
6
6
|
module Ruflet
|
|
7
7
|
module Generators
|
|
8
8
|
class InstallGenerator < ::Rails::Generators::Base
|
|
9
|
+
class_option :frontend, type: :boolean, default: false, desc: "Install the Rails-hosted Ruflet web client"
|
|
10
|
+
class_option :web, type: :boolean, default: false, desc: "Install the Rails-hosted Ruflet web client"
|
|
11
|
+
class_option :desktop, type: :boolean, default: false, desc: "Download the server-driven desktop Ruflet client"
|
|
12
|
+
class_option :client, type: :string, default: nil, desc: "Download prebuilt client from GitHub releases: web, desktop, all, or none"
|
|
13
|
+
|
|
9
14
|
desc "Install Ruflet into a Rails app."
|
|
10
15
|
|
|
11
|
-
def
|
|
12
|
-
target = File.join(destination_root,
|
|
16
|
+
def create_app_entrypoint
|
|
17
|
+
target = File.join(destination_root, entrypoint_path)
|
|
13
18
|
return if File.exist?(target)
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
File.write(
|
|
17
|
-
target,
|
|
18
|
-
Ruflet::Rails::InstallSupport.default_mobile_app_template(app_title: app_name)
|
|
19
|
-
)
|
|
20
|
+
create_file target, Ruflet::Rails::InstallSupport.default_app_template(app_title: app_name)
|
|
20
21
|
end
|
|
21
22
|
|
|
22
23
|
def create_ruflet_yaml
|
|
23
24
|
target = File.join(destination_root, "ruflet.yaml")
|
|
24
25
|
return if File.exist?(target)
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
target,
|
|
28
|
-
Ruflet::Rails::InstallSupport.default_ruflet_yaml(app_name: app_name)
|
|
29
|
-
)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def copy_client_template
|
|
33
|
-
copied = Ruflet::Rails::InstallSupport.copy_ruflet_client_template(destination_root)
|
|
34
|
-
say_status(:warn, "ruflet_client template not found; add it manually before building", :yellow) unless copied
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def configure_client_template
|
|
38
|
-
Ruflet::Rails::InstallSupport.configure_ruflet_client(destination_root)
|
|
27
|
+
create_file target, Ruflet::Rails::InstallSupport.default_ruflet_yaml(app_name: app_name)
|
|
39
28
|
end
|
|
40
29
|
|
|
41
30
|
def add_routes
|
|
42
31
|
target = File.join(destination_root, "config/routes.rb")
|
|
43
32
|
return unless File.file?(target)
|
|
44
33
|
|
|
45
|
-
route = Ruflet::Rails::InstallSupport.route_snippet
|
|
34
|
+
route = Ruflet::Rails::InstallSupport.route_snippet(entrypoint: entrypoint_path)
|
|
46
35
|
return if File.read(target).include?(route)
|
|
47
36
|
|
|
48
37
|
insert_into_file target, " #{route}\n", after: /Rails\.application\.routes\.draw do\s*\n/
|
|
49
38
|
end
|
|
50
39
|
|
|
40
|
+
def add_desktop_flag_to_binstubs
|
|
41
|
+
return unless desktop_requested?
|
|
42
|
+
|
|
43
|
+
install_desktop_flag_bootstrap("bin/rails")
|
|
44
|
+
install_desktop_flag_bootstrap("bin/dev")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def download_prebuilt_client
|
|
48
|
+
client = requested_client
|
|
49
|
+
@web_client_published = false
|
|
50
|
+
return if client == "none"
|
|
51
|
+
|
|
52
|
+
if %w[web all].include?(client)
|
|
53
|
+
@web_client_published = Ruflet::Rails::InstallSupport.publish_web_build(destination_root)
|
|
54
|
+
if @web_client_published
|
|
55
|
+
say "Ruflet web build copied from build/web to public/#{Ruflet::Rails::InstallSupport.default_web_public_path}"
|
|
56
|
+
return if client == "web"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
require "ruflet/cli"
|
|
61
|
+
exit_code = Dir.chdir(destination_root) do
|
|
62
|
+
Ruflet::CLI.command_update([client])
|
|
63
|
+
end
|
|
64
|
+
unless exit_code.to_i.zero?
|
|
65
|
+
@client_download_failed = true
|
|
66
|
+
say_status(:warn, "Ruflet client download failed; install files were generated and build/update steps are printed below", :yellow)
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return unless %w[web all].include?(client)
|
|
71
|
+
|
|
72
|
+
published = Ruflet::Rails::InstallSupport.publish_prebuilt_web_client(destination_root)
|
|
73
|
+
@web_client_published = published
|
|
74
|
+
if published
|
|
75
|
+
say "Ruflet web client published at /#{Ruflet::Rails::InstallSupport.default_web_public_path}/"
|
|
76
|
+
else
|
|
77
|
+
say_status(:warn, "Ruflet web client downloaded, but no prebuilt web index.html was found to publish", :yellow)
|
|
78
|
+
end
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
@client_download_failed = true
|
|
81
|
+
say_status(:warn, "Ruflet client download failed: #{e.class}: #{e.message}", :yellow)
|
|
82
|
+
end
|
|
83
|
+
|
|
51
84
|
def print_install_status
|
|
52
|
-
|
|
85
|
+
Ruflet::Rails::InstallSupport.install_next_steps(
|
|
86
|
+
target: install_target,
|
|
87
|
+
entrypoint: entrypoint_path,
|
|
88
|
+
client: requested_client,
|
|
89
|
+
web_published: !!@web_client_published
|
|
90
|
+
).each { |line| say line }
|
|
53
91
|
end
|
|
54
92
|
|
|
55
93
|
private
|
|
@@ -57,6 +95,57 @@ module Ruflet
|
|
|
57
95
|
def app_name
|
|
58
96
|
File.basename(destination_root).gsub(/[_-]+/, " ").split.map(&:capitalize).join(" ")
|
|
59
97
|
end
|
|
98
|
+
|
|
99
|
+
def entrypoint_path
|
|
100
|
+
Ruflet::Rails::InstallSupport.default_entrypoint_path
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def requested_client
|
|
104
|
+
explicit = options[:client].to_s.strip.downcase
|
|
105
|
+
unless explicit.empty?
|
|
106
|
+
raise Thor::Error, "--client must be web, desktop, all, or none" unless %w[web desktop all none].include?(explicit)
|
|
107
|
+
|
|
108
|
+
return explicit
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
wants_web = options[:frontend] || options[:web]
|
|
112
|
+
wants_desktop = options[:desktop]
|
|
113
|
+
return "all" if wants_web && wants_desktop
|
|
114
|
+
return "web" if wants_web
|
|
115
|
+
return "desktop" if wants_desktop
|
|
116
|
+
|
|
117
|
+
"none"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def desktop_requested?
|
|
121
|
+
%w[desktop all].include?(requested_client)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def install_target
|
|
125
|
+
"ruflet"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def install_desktop_flag_bootstrap(relative_path)
|
|
129
|
+
target = File.join(destination_root, relative_path)
|
|
130
|
+
return unless File.file?(target)
|
|
131
|
+
|
|
132
|
+
source = File.read(target)
|
|
133
|
+
return if source.include?("ruflet_rails desktop flag")
|
|
134
|
+
|
|
135
|
+
bootstrap =
|
|
136
|
+
if source.start_with?("#!/usr/bin/env ruby") || source.start_with?("#!/usr/bin/ruby")
|
|
137
|
+
if File.basename(relative_path) == "dev"
|
|
138
|
+
Ruflet::Rails::InstallSupport.ruby_dev_desktop_flag_bootstrap
|
|
139
|
+
else
|
|
140
|
+
Ruflet::Rails::InstallSupport.ruby_desktop_flag_bootstrap
|
|
141
|
+
end
|
|
142
|
+
elsif source.start_with?("#!/usr/bin/env sh") || source.start_with?("#!/bin/sh")
|
|
143
|
+
Ruflet::Rails::InstallSupport.shell_desktop_flag_bootstrap
|
|
144
|
+
end
|
|
145
|
+
return unless bootstrap
|
|
146
|
+
|
|
147
|
+
insert_into_file target, "#{bootstrap}\n", after: /\A#!.*\n/
|
|
148
|
+
end
|
|
60
149
|
end
|
|
61
150
|
end
|
|
62
151
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "active_support/core_ext/string/inflections"
|
|
5
|
+
require "ruflet/rails/install_support"
|
|
6
|
+
|
|
7
|
+
module Ruflet
|
|
8
|
+
module Generators
|
|
9
|
+
class ScaffoldGenerator < ::Rails::Generators::Base
|
|
10
|
+
argument :model_name, type: :string
|
|
11
|
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
|
|
12
|
+
|
|
13
|
+
desc "Generate a Rails-first Ruflet resource view for an existing model."
|
|
14
|
+
|
|
15
|
+
def create_ruflet_resource_view
|
|
16
|
+
create_file(
|
|
17
|
+
File.join(destination_root, scaffold_view_path),
|
|
18
|
+
Ruflet::Rails::InstallSupport.scaffold_view_template(
|
|
19
|
+
model_name: model_name,
|
|
20
|
+
attributes: scaffold_attributes
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_ruflet_resource_component
|
|
26
|
+
create_file(
|
|
27
|
+
File.join(destination_root, scaffold_component_path),
|
|
28
|
+
Ruflet::Rails::InstallSupport.scaffold_component_template(
|
|
29
|
+
model_name: model_name,
|
|
30
|
+
attributes: scaffold_attributes
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def print_scaffold_status
|
|
36
|
+
say "Ruflet scaffold generated at #{scaffold_view_path}"
|
|
37
|
+
say "Ruflet UI component generated at #{scaffold_component_path}"
|
|
38
|
+
say "The generated view owns the resource logic; the generated component owns the UI."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def scaffold_view_path
|
|
44
|
+
Ruflet::Rails::InstallSupport.scaffold_view_path(model_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def scaffold_component_path
|
|
48
|
+
Ruflet::Rails::InstallSupport.scaffold_component_path(model_name)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def scaffold_attributes
|
|
52
|
+
return attributes unless attributes.empty?
|
|
53
|
+
|
|
54
|
+
model_class = model_name.to_s.camelize.safe_constantize
|
|
55
|
+
inferred = Ruflet::Rails::InstallSupport.attributes_from_model(model_class)
|
|
56
|
+
inferred.empty? ? attributes : inferred
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Ruflet
|
|
6
|
+
module Rails
|
|
7
|
+
module DesktopLauncher
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def launch_once(root:, argv: ARGV, env: ENV, wait: 1.0)
|
|
11
|
+
return false unless desktop_enabled?(root: root, argv: argv, env: env)
|
|
12
|
+
return false unless env["RUFLET_RAILS_DESKTOP_SERVER"].to_s.downcase == "true" || rails_server_command?(argv)
|
|
13
|
+
return false if env["RUFLET_RAILS_DESKTOP"].to_s.downcase == "false"
|
|
14
|
+
return false if launched?
|
|
15
|
+
|
|
16
|
+
@launched = true
|
|
17
|
+
url = backend_url(root: root, argv: argv, env: env)
|
|
18
|
+
Thread.new do
|
|
19
|
+
sleep wait.to_f if wait.to_f.positive?
|
|
20
|
+
launch(url, root: root)
|
|
21
|
+
end
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def launch(url, root:)
|
|
26
|
+
require "ruflet/cli"
|
|
27
|
+
|
|
28
|
+
Dir.chdir(root.to_s) do
|
|
29
|
+
wait_for_backend(url)
|
|
30
|
+
if Ruflet::CLI.respond_to?(:launch_desktop_client, true)
|
|
31
|
+
Ruflet::CLI.send(:launch_desktop_client, url)
|
|
32
|
+
else
|
|
33
|
+
warn "Ruflet desktop launcher is unavailable in this ruflet version."
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
rescue StandardError => e
|
|
38
|
+
warn "Failed to launch Ruflet desktop client: #{e.class}: #{e.message}"
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def backend_url(root:, argv: ARGV, env: ENV)
|
|
43
|
+
explicit = env["RUFLET_BACKEND_URL"].to_s.strip
|
|
44
|
+
return explicit unless explicit.empty?
|
|
45
|
+
|
|
46
|
+
config_url = ruflet_yaml_backend_url(root)
|
|
47
|
+
cli_port = rails_server_port(argv)
|
|
48
|
+
return replace_url_port(config_url, cli_port) if cli_port
|
|
49
|
+
return config_url if config_url
|
|
50
|
+
|
|
51
|
+
"http://localhost:#{env["PORT"].to_s.empty? ? 3000 : env["PORT"]}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def desktop_enabled?(root:, argv: ARGV, env: ENV)
|
|
55
|
+
return true if env["RUFLET_RAILS_DESKTOP_SERVER"].to_s.downcase == "true"
|
|
56
|
+
return true if env["RUFLET_RAILS_DESKTOP"].to_s.downcase == "true"
|
|
57
|
+
return true if Array(argv).map(&:to_s).include?("--desktop")
|
|
58
|
+
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rails_server_command?(argv)
|
|
63
|
+
command = Array(argv).find { |value| !value.to_s.start_with?("-") }.to_s
|
|
64
|
+
%w[server s].include?(command)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def rails_server_port(argv)
|
|
68
|
+
values = Array(argv).map(&:to_s)
|
|
69
|
+
values.each_with_index do |value, index|
|
|
70
|
+
return values[index + 1].to_i if %w[-p --port].include?(value) && values[index + 1].to_i.positive?
|
|
71
|
+
return value.split("=", 2).last.to_i if value.start_with?("--port=") && value.split("=", 2).last.to_i.positive?
|
|
72
|
+
end
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ruflet_yaml_backend_url(root)
|
|
77
|
+
path = %w[ruflet.yaml ruflet.yml].map { |name| File.join(root.to_s, name) }.find { |candidate| File.file?(candidate) }
|
|
78
|
+
return nil unless path
|
|
79
|
+
|
|
80
|
+
require "yaml"
|
|
81
|
+
config = YAML.safe_load(File.read(path), aliases: true) || {}
|
|
82
|
+
value = config.dig("app", "backend_url") || config["backend_url"] || config["server_url"]
|
|
83
|
+
value.to_s.strip.empty? ? nil : value.to_s.strip
|
|
84
|
+
rescue StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def replace_url_port(url, port)
|
|
89
|
+
return "http://localhost:#{port}" if url.to_s.strip.empty?
|
|
90
|
+
|
|
91
|
+
uri = URI.parse(url)
|
|
92
|
+
uri.port = port.to_i
|
|
93
|
+
uri.to_s
|
|
94
|
+
rescue URI::InvalidURIError
|
|
95
|
+
"http://localhost:#{port}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def wait_for_backend(url)
|
|
99
|
+
return unless Ruflet::CLI.respond_to?(:wait_for_server_boot, true)
|
|
100
|
+
|
|
101
|
+
uri = URI.parse(url)
|
|
102
|
+
Ruflet::CLI.send(:wait_for_server_boot, uri.port) if uri.port
|
|
103
|
+
rescue URI::InvalidURIError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def launched?
|
|
108
|
+
!!@launched
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def reset!
|
|
112
|
+
@launched = false
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|