terrazzo 0.6.0 → 0.7.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 +4 -4
- data/README.md +101 -0
- data/Rakefile +1 -0
- data/app/controllers/terrazzo/application_controller.rb +63 -14
- data/app/helpers/terrazzo/collection_actions_helper.rb +38 -13
- data/app/helpers/terrazzo/resource_paths_helper.rb +55 -0
- data/app/views/terrazzo/application/_edit_base.json.props +7 -4
- data/app/views/terrazzo/application/_index_base.json.props +83 -7
- data/app/views/terrazzo/application/_navigation.json.props +7 -4
- data/app/views/terrazzo/application/_new_base.json.props +6 -3
- data/app/views/terrazzo/application/_show_base.json.props +7 -5
- data/lib/generators/terrazzo/dashboard/dashboard_generator.rb +48 -24
- data/lib/generators/terrazzo/dashboard/templates/controller.rb.erb +8 -4
- data/lib/generators/terrazzo/dashboard/templates/dashboard.rb.erb +33 -25
- data/lib/generators/terrazzo/eject/eject_generator.rb +223 -113
- data/lib/generators/terrazzo/field/field_generator.rb +51 -7
- data/lib/generators/terrazzo/field/templates/FormField.jsx.erb +2 -3
- data/lib/generators/terrazzo/install/install_generator.rb +641 -4
- data/lib/generators/terrazzo/install/templates/admin.css +2 -0
- data/lib/generators/terrazzo/install/templates/application.js.erb +3 -0
- data/lib/generators/terrazzo/install/templates/components.json.erb +17 -0
- data/lib/generators/terrazzo/install/templates/custom_page_mapping.js.erb +5 -0
- data/lib/generators/terrazzo/install/templates/generated_page_mapping.js.erb +10 -0
- data/lib/generators/terrazzo/install/templates/jsconfig.json +8 -0
- data/lib/generators/terrazzo/install/templates/page_to_page_mapping.js.erb +9 -1
- data/lib/generators/terrazzo/install/templates/superglue.html.erb.erb +2 -2
- data/lib/generators/terrazzo/routes/routes_generator.rb +55 -20
- data/lib/generators/terrazzo/views/edit_generator.rb +16 -10
- data/lib/generators/terrazzo/views/eject_compatibility.rb +19 -0
- data/lib/generators/terrazzo/views/field_generator.rb +18 -6
- data/lib/generators/terrazzo/views/generated_defaults_helper.rb +110 -0
- data/lib/generators/terrazzo/views/index_generator.rb +9 -8
- data/lib/generators/terrazzo/views/layout_generator.rb +4 -3
- data/lib/generators/terrazzo/views/navigation_generator.rb +5 -1
- data/lib/generators/terrazzo/views/new_generator.rb +16 -10
- data/lib/generators/terrazzo/views/page_mapping_helper.rb +131 -8
- data/lib/generators/terrazzo/views/show_generator.rb +7 -6
- data/lib/generators/terrazzo/views/templates/components/CollectionFilters.jsx +25 -0
- data/lib/generators/terrazzo/views/templates/components/CollectionItemActions.jsx +68 -0
- data/lib/generators/terrazzo/views/templates/components/CollectionToolbarActions.jsx +60 -0
- data/lib/generators/terrazzo/views/templates/components/HasManyPagination.jsx +36 -0
- data/lib/generators/terrazzo/views/templates/components/Layout.jsx +9 -4
- data/lib/generators/terrazzo/views/templates/components/Pagination.jsx +32 -24
- data/lib/generators/terrazzo/views/templates/components/ResourceTable.jsx +172 -0
- data/lib/generators/terrazzo/views/templates/components/SearchBar.jsx +14 -6
- data/lib/generators/terrazzo/views/templates/components/SortableHeader.jsx +7 -5
- data/lib/generators/terrazzo/views/templates/components/app-sidebar.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/components/site-header.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/asset/FormField.jsx +15 -0
- data/lib/generators/terrazzo/views/templates/fields/asset/IndexField.jsx +5 -0
- data/lib/generators/terrazzo/views/templates/fields/asset/ShowField.jsx +13 -0
- data/lib/generators/terrazzo/views/templates/fields/belongs_to/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/boolean/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/boolean/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/has_many/FormField.jsx +3 -3
- data/lib/generators/terrazzo/views/templates/fields/has_many/IndexField.jsx +4 -3
- data/lib/generators/terrazzo/views/templates/fields/has_many/ShowField.jsx +21 -35
- data/lib/generators/terrazzo/views/templates/fields/has_one/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/hstore/FormField.jsx +5 -5
- data/lib/generators/terrazzo/views/templates/fields/hstore/IndexField.jsx +0 -2
- data/lib/generators/terrazzo/views/templates/fields/hstore/ShowField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/number/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/number/ShowField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/polymorphic/FormField.jsx +3 -2
- data/lib/generators/terrazzo/views/templates/fields/rich_text/FormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/select/FormField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/select/IndexField.jsx +1 -1
- data/lib/generators/terrazzo/views/templates/fields/shared/TextInputFormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/fields/text/FormField.jsx +2 -2
- data/lib/generators/terrazzo/views/templates/pages/_collection.jsx +5 -3
- data/lib/generators/terrazzo/views/templates/pages/_form.jsx +37 -30
- data/lib/generators/terrazzo/views/templates/pages/_navigation.json.props +14 -4
- data/lib/generators/terrazzo/views/templates/pages/edit.jsx +6 -4
- data/lib/generators/terrazzo/views/templates/pages/index.jsx +20 -8
- data/lib/generators/terrazzo/views/templates/pages/new.jsx +8 -4
- data/lib/generators/terrazzo/views/templates/pages/show.jsx +30 -32
- data/lib/generators/terrazzo/views/views_generator.rb +9 -5
- data/lib/terrazzo/base_dashboard.rb +167 -5
- data/lib/terrazzo/csv_export.rb +38 -0
- data/lib/terrazzo/field/associative.rb +16 -4
- data/lib/terrazzo/field/base.rb +4 -0
- data/lib/terrazzo/field/belongs_to.rb +1 -2
- data/lib/terrazzo/field/deferred.rb +4 -0
- data/lib/terrazzo/field/has_many.rb +79 -11
- data/lib/terrazzo/field/password.rb +6 -0
- data/lib/terrazzo/field/rich_text.rb +13 -0
- data/lib/terrazzo/generator_helpers.rb +1 -1
- data/lib/terrazzo/has_many_pagination.rb +9 -1
- data/lib/terrazzo/namespace/resource.rb +30 -0
- data/lib/terrazzo/namespace.rb +26 -1
- data/lib/terrazzo/search.rb +29 -9
- data/lib/terrazzo/version.rb +1 -1
- data/lib/terrazzo.rb +1 -0
- data/terrazzo.gemspec +11 -1
- metadata +40 -6
- data/lib/generators/terrazzo/install/templates/application.json.props.erb +0 -17
- data/lib/generators/terrazzo/views/templates/fields/FieldRenderer.jsx +0 -103
- data/lib/generators/terrazzo/views/templates/fields/index.js +0 -81
|
@@ -1,14 +1,132 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "pathname"
|
|
1
3
|
require "rails/generators"
|
|
4
|
+
require "terrazzo/version"
|
|
2
5
|
|
|
3
6
|
module Terrazzo
|
|
4
7
|
module Generators
|
|
5
8
|
class InstallGenerator < Rails::Generators::Base
|
|
6
9
|
source_root File.expand_path("templates", __dir__)
|
|
7
10
|
|
|
11
|
+
TAILWIND_PACKAGE_NAME = "tailwindcss"
|
|
12
|
+
TAILWIND_CLI_PACKAGE_NAME = "@tailwindcss/cli"
|
|
13
|
+
TAILWIND_VITE_PACKAGE_NAME = "@tailwindcss/vite"
|
|
14
|
+
TAILWIND_RAILS_GEM_NAME = "tailwindcss-rails"
|
|
15
|
+
TAILWIND_MINIMUM_VERSION = "4.0.0"
|
|
16
|
+
|
|
17
|
+
FRONTEND_DEPENDENCIES = (
|
|
18
|
+
%w[
|
|
19
|
+
terrazzo
|
|
20
|
+
react
|
|
21
|
+
react-dom
|
|
22
|
+
react-redux
|
|
23
|
+
@reduxjs/toolkit
|
|
24
|
+
@thoughtbot/superglue
|
|
25
|
+
@radix-ui/react-avatar
|
|
26
|
+
@radix-ui/react-dialog
|
|
27
|
+
@radix-ui/react-dropdown-menu
|
|
28
|
+
@radix-ui/react-label
|
|
29
|
+
@radix-ui/react-popover
|
|
30
|
+
@radix-ui/react-select
|
|
31
|
+
@radix-ui/react-separator
|
|
32
|
+
@radix-ui/react-slot
|
|
33
|
+
@radix-ui/react-tooltip
|
|
34
|
+
class-variance-authority
|
|
35
|
+
lucide-react
|
|
36
|
+
] + [TAILWIND_PACKAGE_NAME]
|
|
37
|
+
).freeze
|
|
38
|
+
FRONTEND_DEPENDENCY_MINIMUMS = {
|
|
39
|
+
"terrazzo" => Terrazzo::VERSION,
|
|
40
|
+
"react" => "18.0.0",
|
|
41
|
+
"react-dom" => "18.0.0",
|
|
42
|
+
"react-redux" => "8.0.0",
|
|
43
|
+
"@reduxjs/toolkit" => "1.9.0",
|
|
44
|
+
"@thoughtbot/superglue" => "1.0.0",
|
|
45
|
+
"@radix-ui/react-avatar" => "1.0.0",
|
|
46
|
+
"@radix-ui/react-dialog" => "1.0.0",
|
|
47
|
+
"@radix-ui/react-dropdown-menu" => "2.0.0",
|
|
48
|
+
"@radix-ui/react-label" => "2.0.0",
|
|
49
|
+
"@radix-ui/react-popover" => "1.0.0",
|
|
50
|
+
"@radix-ui/react-select" => "2.0.0",
|
|
51
|
+
"@radix-ui/react-separator" => "1.0.0",
|
|
52
|
+
"@radix-ui/react-slot" => "1.0.0",
|
|
53
|
+
"@radix-ui/react-tooltip" => "1.0.0",
|
|
54
|
+
"class-variance-authority" => "0.7.0",
|
|
55
|
+
"lucide-react" => "0.300.0",
|
|
56
|
+
TAILWIND_PACKAGE_NAME => TAILWIND_MINIMUM_VERSION,
|
|
57
|
+
}.freeze
|
|
58
|
+
VITE_FRONTEND_DEPENDENCIES = %w[
|
|
59
|
+
vite
|
|
60
|
+
vite-plugin-ruby
|
|
61
|
+
].freeze
|
|
62
|
+
SUPPORTED_BUNDLERS = %w[vite esbuild].freeze
|
|
63
|
+
|
|
64
|
+
argument :namespace_argument, type: :string, required: false,
|
|
65
|
+
desc: "Admin namespace. Prefer --namespace; this positional form is kept for compatibility."
|
|
66
|
+
|
|
8
67
|
class_option :namespace, type: :string, default: "admin",
|
|
9
68
|
desc: "Admin namespace"
|
|
10
69
|
class_option :bundler, type: :string, default: "vite",
|
|
11
|
-
desc: "JavaScript bundler (vite or
|
|
70
|
+
desc: "JavaScript bundler (vite or esbuild)"
|
|
71
|
+
|
|
72
|
+
def validate_bundler
|
|
73
|
+
return if SUPPORTED_BUNDLERS.include?(options[:bundler])
|
|
74
|
+
|
|
75
|
+
raise Thor::Error, <<~MESSAGE
|
|
76
|
+
Unsupported bundler '#{options[:bundler]}'.
|
|
77
|
+
|
|
78
|
+
Terrazzo generates React/JSX admin views and supports Vite or esbuild.
|
|
79
|
+
Re-run with `--bundler=vite` or `--bundler=esbuild`.
|
|
80
|
+
MESSAGE
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def validate_namespace
|
|
84
|
+
return if namespace_argument.blank?
|
|
85
|
+
return if options[:namespace] == "admin" || options[:namespace] == namespace_argument
|
|
86
|
+
|
|
87
|
+
raise Thor::Error, <<~MESSAGE
|
|
88
|
+
Conflicting admin namespaces: '#{namespace_argument}' and '#{options[:namespace]}'.
|
|
89
|
+
|
|
90
|
+
Use either `rails generate terrazzo:install #{namespace_argument}` or
|
|
91
|
+
`rails generate terrazzo:install --namespace=#{options[:namespace]}`, not both.
|
|
92
|
+
MESSAGE
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def verify_vite_bundler
|
|
96
|
+
return unless vite?
|
|
97
|
+
|
|
98
|
+
unless vite_rails_installed? && parsed_vite_config
|
|
99
|
+
raise Thor::Error, <<~MESSAGE
|
|
100
|
+
Terrazzo is configured for Vite, but vite_rails is not installed and configured.
|
|
101
|
+
|
|
102
|
+
Add `gem "vite_rails"` and run `bundle exec vite install` first, or re-run Terrazzo with `--bundler=esbuild`.
|
|
103
|
+
MESSAGE
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
missing = missing_vite_frontend_dependencies
|
|
107
|
+
return if missing.empty?
|
|
108
|
+
|
|
109
|
+
raise Thor::Error, <<~MESSAGE
|
|
110
|
+
Terrazzo is configured for Vite, but package.json is missing required Vite dependencies: #{missing.join(", ")}.
|
|
111
|
+
|
|
112
|
+
Run `bundle exec vite install` successfully, install the generated package.json dependencies, or re-run Terrazzo with `--bundler=esbuild`.
|
|
113
|
+
MESSAGE
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def verify_database_schema
|
|
117
|
+
models = application_models
|
|
118
|
+
raise_no_models_error if models.empty?
|
|
119
|
+
|
|
120
|
+
missing_tables = models.reject { |model| table_exists_for?(model) }
|
|
121
|
+
return if missing_tables.empty?
|
|
122
|
+
|
|
123
|
+
model_names = missing_tables.map(&:name).join(", ")
|
|
124
|
+
raise Thor::Error, <<~MESSAGE
|
|
125
|
+
Terrazzo could not generate dashboards because these models do not have database tables: #{model_names}
|
|
126
|
+
|
|
127
|
+
Run `bin/rails db:prepare` first, then run `bin/rails generate terrazzo:install` again.
|
|
128
|
+
MESSAGE
|
|
129
|
+
end
|
|
12
130
|
|
|
13
131
|
def create_application_controller
|
|
14
132
|
template "application_controller.rb.erb",
|
|
@@ -26,8 +144,47 @@ module Terrazzo
|
|
|
26
144
|
end
|
|
27
145
|
|
|
28
146
|
def create_js_entry_point
|
|
29
|
-
template "application.js.erb",
|
|
30
|
-
|
|
147
|
+
template "application.js.erb", js_entry_point_path
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def create_vite_entry_point
|
|
151
|
+
return unless vite?
|
|
152
|
+
return if vite_entry_point_path == js_entry_point_path
|
|
153
|
+
|
|
154
|
+
create_file vite_entry_point_path,
|
|
155
|
+
%(import "#{relative_import_path(vite_entry_point_directory, js_entry_point_path)}"\n)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def configure_vite_watch_paths
|
|
159
|
+
return unless vite?
|
|
160
|
+
|
|
161
|
+
config = parsed_vite_config
|
|
162
|
+
return unless config
|
|
163
|
+
|
|
164
|
+
watch_paths = vite_watch_paths
|
|
165
|
+
return if watch_paths.empty?
|
|
166
|
+
|
|
167
|
+
config["all"] = {} unless config["all"].is_a?(Hash)
|
|
168
|
+
existing_paths = Array(config["all"]["watchAdditionalPaths"])
|
|
169
|
+
updated_paths = existing_paths | watch_paths
|
|
170
|
+
return if updated_paths == existing_paths
|
|
171
|
+
|
|
172
|
+
config["all"]["watchAdditionalPaths"] = updated_paths
|
|
173
|
+
create_file "config/vite.json", "#{JSON.pretty_generate(config)}\n", force: true
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def configure_vite_dev_server
|
|
177
|
+
return unless vite?
|
|
178
|
+
|
|
179
|
+
ensure_vite_procfile
|
|
180
|
+
ensure_vite_bin_dev
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def create_esbuild_entry_point
|
|
184
|
+
return unless esbuild?
|
|
185
|
+
|
|
186
|
+
create_file "app/javascript/#{namespace_name}.js",
|
|
187
|
+
%(import "./#{namespace_name}/application.jsx"\n)
|
|
31
188
|
end
|
|
32
189
|
|
|
33
190
|
def create_store
|
|
@@ -40,6 +197,16 @@ module Terrazzo
|
|
|
40
197
|
"app/javascript/#{namespace_name}/page_to_page_mapping.js"
|
|
41
198
|
end
|
|
42
199
|
|
|
200
|
+
def create_generated_page_mapping
|
|
201
|
+
template "generated_page_mapping.js.erb",
|
|
202
|
+
"app/javascript/#{namespace_name}/generated_page_mapping.js"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def create_custom_page_mapping
|
|
206
|
+
template "custom_page_mapping.js.erb",
|
|
207
|
+
"app/javascript/#{namespace_name}/custom_page_mapping.js"
|
|
208
|
+
end
|
|
209
|
+
|
|
43
210
|
def create_application_visit
|
|
44
211
|
template "application_visit.js.erb",
|
|
45
212
|
"app/javascript/#{namespace_name}/application_visit.js"
|
|
@@ -55,6 +222,66 @@ module Terrazzo
|
|
|
55
222
|
"app/assets/stylesheets/#{namespace_name}.css"
|
|
56
223
|
end
|
|
57
224
|
|
|
225
|
+
def create_tailwind_build_script
|
|
226
|
+
return unless package_json_file?
|
|
227
|
+
return if tailwind_build_pipeline?
|
|
228
|
+
return unless tailwind_cli_package?
|
|
229
|
+
return unless tailwind_cli_supported?
|
|
230
|
+
|
|
231
|
+
package_json = parsed_package_json
|
|
232
|
+
return if package_json.empty?
|
|
233
|
+
|
|
234
|
+
scripts = package_json["scripts"]
|
|
235
|
+
scripts = package_json["scripts"] = {} unless scripts.is_a?(Hash)
|
|
236
|
+
return if scripts.key?(tailwind_build_script_name)
|
|
237
|
+
|
|
238
|
+
scripts[tailwind_build_script_name] = tailwind_build_command
|
|
239
|
+
create_file "package.json", "#{JSON.pretty_generate(package_json)}\n", force: true
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def create_vite_build_script
|
|
243
|
+
return unless vite?
|
|
244
|
+
return unless package_json_file?
|
|
245
|
+
|
|
246
|
+
package_json = parsed_package_json
|
|
247
|
+
return if package_json.empty?
|
|
248
|
+
|
|
249
|
+
scripts = package_json["scripts"]
|
|
250
|
+
scripts = package_json["scripts"] = {} unless scripts.is_a?(Hash)
|
|
251
|
+
return if scripts.key?("build")
|
|
252
|
+
|
|
253
|
+
build_commands = ["vite build"]
|
|
254
|
+
if scripts[tailwind_build_script_name] == tailwind_build_command
|
|
255
|
+
build_commands << tailwind_build_command
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
scripts["build"] = build_commands.join(" && ")
|
|
259
|
+
create_file "package.json", "#{JSON.pretty_generate(package_json)}\n", force: true
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def create_components_json
|
|
263
|
+
if File.exist?(File.join(destination_root, "components.json"))
|
|
264
|
+
say_status :skip, "components.json already exists", :yellow
|
|
265
|
+
return
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
template "components.json.erb", "components.json"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def create_jsconfig
|
|
272
|
+
if File.exist?(File.join(destination_root, "jsconfig.json"))
|
|
273
|
+
say_status :skip, "jsconfig.json already exists", :yellow
|
|
274
|
+
return
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
if File.exist?(File.join(destination_root, "tsconfig.json"))
|
|
278
|
+
say_status :skip, "tsconfig.json already exists", :yellow
|
|
279
|
+
return
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
copy_file "jsconfig.json", "jsconfig.json"
|
|
283
|
+
end
|
|
284
|
+
|
|
58
285
|
def run_views_generator
|
|
59
286
|
generate "terrazzo:views", "--namespace=#{namespace_name}"
|
|
60
287
|
end
|
|
@@ -69,16 +296,410 @@ module Terrazzo
|
|
|
69
296
|
end
|
|
70
297
|
end
|
|
71
298
|
|
|
299
|
+
def verify_frontend_dependencies
|
|
300
|
+
missing = missing_frontend_dependencies
|
|
301
|
+
outdated = outdated_frontend_dependencies
|
|
302
|
+
|
|
303
|
+
if missing.any?
|
|
304
|
+
say_status :warning, "Missing frontend dependencies required by Terrazzo:", :yellow
|
|
305
|
+
say " #{missing.join(" ")}"
|
|
306
|
+
say "Run: #{frontend_install_command(missing)}"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
return if outdated.empty?
|
|
310
|
+
|
|
311
|
+
say_status :warning, "Frontend dependencies below Terrazzo's supported versions:", :yellow
|
|
312
|
+
outdated.each do |dependency|
|
|
313
|
+
say " #{dependency[:name]} #{dependency[:requirement]} (requires >= #{dependency[:minimum]})"
|
|
314
|
+
end
|
|
315
|
+
say "Run: #{frontend_install_command(outdated.map { |dependency| "#{dependency[:name]}@latest" })}"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def verify_tailwind_build_pipeline
|
|
319
|
+
if tailwind_cli_package? && !tailwind_cli_supported?
|
|
320
|
+
say_status :warning, "Installed #{TAILWIND_CLI_PACKAGE_NAME} is below Terrazzo's supported version.", :yellow
|
|
321
|
+
say "Terrazzo generated app/assets/stylesheets/#{namespace_name}.css with Tailwind #{TAILWIND_MINIMUM_VERSION}+ syntax."
|
|
322
|
+
say "Run: #{package_manager_add_dev_command} #{TAILWIND_CLI_PACKAGE_NAME}@latest"
|
|
323
|
+
return
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
if tailwind_rails_gem? && !tailwind_rails_gem_supported?
|
|
327
|
+
say_status :warning, "Installed #{TAILWIND_RAILS_GEM_NAME} is below Terrazzo's supported version.", :yellow
|
|
328
|
+
say "Terrazzo generated app/assets/stylesheets/#{namespace_name}.css with Tailwind #{TAILWIND_MINIMUM_VERSION}+ syntax."
|
|
329
|
+
say "Update #{TAILWIND_RAILS_GEM_NAME} to #{TAILWIND_MINIMUM_VERSION} or newer, or install #{TAILWIND_CLI_PACKAGE_NAME}@latest."
|
|
330
|
+
return
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
return if tailwind_build_pipeline?
|
|
334
|
+
|
|
335
|
+
say_status :warning, "Terrazzo generated app/assets/stylesheets/#{namespace_name}.css but no Tailwind build pipeline was detected.", :yellow
|
|
336
|
+
say "Make sure your app compiles that file with Tailwind before serving the admin UI."
|
|
337
|
+
if tailwind_vite_plugin_package?
|
|
338
|
+
say "#{TAILWIND_VITE_PACKAGE_NAME} is installed, but Terrazzo links #{admin_stylesheet_path} as a Rails stylesheet asset."
|
|
339
|
+
end
|
|
340
|
+
say "For package.json scripts, install the Tailwind CLI and add a build script:"
|
|
341
|
+
say " #{package_manager_add_dev_command} #{TAILWIND_CLI_PACKAGE_NAME}@latest"
|
|
342
|
+
say %( "#{tailwind_build_script_name}": "#{tailwind_build_command}")
|
|
343
|
+
end
|
|
344
|
+
|
|
72
345
|
private
|
|
73
346
|
|
|
74
347
|
def namespace_name
|
|
75
|
-
options[:namespace]
|
|
348
|
+
namespace_argument.presence || options[:namespace]
|
|
76
349
|
end
|
|
77
350
|
|
|
78
351
|
def vite?
|
|
79
352
|
options[:bundler] == "vite"
|
|
80
353
|
end
|
|
81
354
|
|
|
355
|
+
def esbuild?
|
|
356
|
+
options[:bundler] == "esbuild"
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def js_entry_point_path
|
|
360
|
+
"app/javascript/#{namespace_name}/application.jsx"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def vite_entry_point_path
|
|
364
|
+
clean_relative_path(File.join(vite_source_code_dir, vite_entrypoints_dir, namespace_name, "application.jsx"))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def vite_entry_point_tag_name
|
|
368
|
+
clean_relative_path(File.join(vite_entrypoints_dir, namespace_name, "application.jsx"))
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def vite_entry_point_directory
|
|
372
|
+
File.dirname(vite_entry_point_path)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def vite_source_code_dir
|
|
376
|
+
vite_config.fetch("sourceCodeDir", "app/frontend")
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def vite_entrypoints_dir
|
|
380
|
+
vite_config.fetch("entrypointsDir", "entrypoints")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def vite_config
|
|
384
|
+
@vite_config ||= begin
|
|
385
|
+
raw_config = parsed_vite_config || {}
|
|
386
|
+
raw_config
|
|
387
|
+
.merge(raw_config.fetch("all", {}))
|
|
388
|
+
.merge(raw_config.fetch(vite_environment, {}))
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def vite_environment
|
|
393
|
+
ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence || "development"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def parsed_vite_config
|
|
397
|
+
return nil unless vite_config_installed?
|
|
398
|
+
|
|
399
|
+
JSON.parse(File.read(vite_config_path))
|
|
400
|
+
rescue JSON::ParserError
|
|
401
|
+
nil
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def relative_import_path(from_directory, to_path)
|
|
405
|
+
relative_path = Pathname
|
|
406
|
+
.new(File.join(destination_root, to_path))
|
|
407
|
+
.relative_path_from(Pathname.new(File.join(destination_root, from_directory)))
|
|
408
|
+
.to_s
|
|
409
|
+
|
|
410
|
+
relative_path.start_with?(".") ? relative_path : "./#{relative_path}"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def clean_relative_path(path)
|
|
414
|
+
Pathname.new(path).cleanpath.to_s
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def vite_watch_paths
|
|
418
|
+
[
|
|
419
|
+
"app/javascript/#{namespace_name}/**/*",
|
|
420
|
+
"app/views/#{namespace_name}/**/*",
|
|
421
|
+
].select { |path| outside_vite_source_code_dir?(path) }
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def outside_vite_source_code_dir?(path)
|
|
425
|
+
source_dir = clean_relative_path(vite_source_code_dir)
|
|
426
|
+
return false if source_dir == "."
|
|
427
|
+
|
|
428
|
+
watched_root = clean_relative_path(path.delete_suffix("/**/*"))
|
|
429
|
+
watched_root != source_dir && !watched_root.start_with?("#{source_dir}/")
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def vite_config_installed?
|
|
433
|
+
File.exist?(vite_config_path)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def vite_config_path
|
|
437
|
+
File.join(destination_root, "config/vite.json")
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
def ensure_vite_procfile
|
|
441
|
+
if File.exist?(procfile_dev_path)
|
|
442
|
+
return if procfile_runs_vite?(File.read(procfile_dev_path))
|
|
443
|
+
|
|
444
|
+
existing_content = File.read(procfile_dev_path)
|
|
445
|
+
append_to_file "Procfile.dev",
|
|
446
|
+
"#{existing_content.end_with?("\n") ? "" : "\n"}vite: bin/vite dev\n"
|
|
447
|
+
else
|
|
448
|
+
create_file "Procfile.dev", <<~PROCFILE
|
|
449
|
+
web: bin/rails s
|
|
450
|
+
vite: bin/vite dev
|
|
451
|
+
PROCFILE
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def ensure_vite_bin_dev
|
|
456
|
+
if File.exist?(bin_dev_path)
|
|
457
|
+
content = File.read(bin_dev_path)
|
|
458
|
+
return if bin_dev_runs_procfile?(content)
|
|
459
|
+
|
|
460
|
+
unless default_rails_server_bin_dev?(content)
|
|
461
|
+
say_status :warning, "bin/dev does not run Procfile.dev; start Vite with `bin/vite dev` or update bin/dev.", :yellow
|
|
462
|
+
return
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
create_file "bin/dev", vite_bin_dev_script, force: true
|
|
467
|
+
File.chmod(0o755, bin_dev_path)
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def procfile_dev_path
|
|
471
|
+
File.join(destination_root, "Procfile.dev")
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def bin_dev_path
|
|
475
|
+
File.join(destination_root, "bin/dev")
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def procfile_runs_vite?(content)
|
|
479
|
+
content.match?(/^\s*[^#\n]+:\s*.*(?:bin\/)?vite\s+dev\b/)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def bin_dev_runs_procfile?(content)
|
|
483
|
+
content.include?("Procfile.dev")
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def default_rails_server_bin_dev?(content)
|
|
487
|
+
content.include?('exec "./bin/rails", "server", *ARGV') ||
|
|
488
|
+
content.match?(/exec\s+\.?\/?bin\/rails\s+(server|s)\b/)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def vite_bin_dev_script
|
|
492
|
+
<<~SH
|
|
493
|
+
#!/usr/bin/env sh
|
|
494
|
+
|
|
495
|
+
if ! gem list foreman -i --silent; then
|
|
496
|
+
echo "Installing foreman..."
|
|
497
|
+
gem install foreman
|
|
498
|
+
fi
|
|
499
|
+
|
|
500
|
+
exec foreman start -f Procfile.dev "$@"
|
|
501
|
+
SH
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def vite_rails_installed?
|
|
505
|
+
%w[Gemfile Gemfile.lock].any? do |file_name|
|
|
506
|
+
path = File.join(destination_root, file_name)
|
|
507
|
+
File.exist?(path) && File.read(path).include?("vite_rails")
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def missing_frontend_dependencies
|
|
512
|
+
installed = package_json_dependencies
|
|
513
|
+
FRONTEND_DEPENDENCIES.reject { |package_name| installed.key?(package_name) }
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def outdated_frontend_dependencies
|
|
517
|
+
installed = package_json_dependencies
|
|
518
|
+
FRONTEND_DEPENDENCY_MINIMUMS.filter_map do |package_name, minimum|
|
|
519
|
+
requirement = installed[package_name]
|
|
520
|
+
next if requirement.nil?
|
|
521
|
+
next if dependency_requirement_satisfies_minimum?(requirement, minimum)
|
|
522
|
+
|
|
523
|
+
{
|
|
524
|
+
name: package_name,
|
|
525
|
+
requirement: requirement,
|
|
526
|
+
minimum: minimum,
|
|
527
|
+
}
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def dependency_requirement_satisfies_minimum?(requirement, minimum)
|
|
532
|
+
requirement = requirement.to_s.strip
|
|
533
|
+
return true if requirement.blank? || requirement == "*" || requirement.downcase == "latest"
|
|
534
|
+
return true if non_registry_dependency_requirement?(requirement)
|
|
535
|
+
|
|
536
|
+
minimum_version = Gem::Version.new(minimum)
|
|
537
|
+
requirement.split("||").any? do |range|
|
|
538
|
+
floor = dependency_requirement_floor(range)
|
|
539
|
+
floor && Gem::Version.new(floor) >= minimum_version
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def dependency_requirement_floor(range)
|
|
544
|
+
range
|
|
545
|
+
.split(/[,\s]+/)
|
|
546
|
+
.filter_map { |token| dependency_requirement_token_version(token) }
|
|
547
|
+
.first
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def dependency_requirement_token_version(token)
|
|
551
|
+
return nil if token.blank? || token.start_with?("<")
|
|
552
|
+
|
|
553
|
+
match = token.match(/\A(?:>=|>|=|~>|\^|~)?v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?(?:[-+][0-9A-Za-z.-]+)?\z/i)
|
|
554
|
+
return nil unless match
|
|
555
|
+
|
|
556
|
+
[match[1], match[2], match[3]]
|
|
557
|
+
.map { |part| part.presence&.then { |value| %w[x *].include?(value.downcase) ? "0" : value } || "0" }
|
|
558
|
+
.join(".")
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def non_registry_dependency_requirement?(requirement)
|
|
562
|
+
requirement.match?(/\A(?:file|link|workspace|git|github|http|https|npm):/) ||
|
|
563
|
+
requirement.match?(/\Agit\+/)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def missing_vite_frontend_dependencies
|
|
567
|
+
installed = package_json_dependencies
|
|
568
|
+
VITE_FRONTEND_DEPENDENCIES.reject { |package_name| installed.key?(package_name) }
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def package_json_dependencies
|
|
572
|
+
return {} unless package_json_file?
|
|
573
|
+
|
|
574
|
+
package_json = parsed_package_json
|
|
575
|
+
%w[dependencies devDependencies peerDependencies optionalDependencies]
|
|
576
|
+
.each_with_object({}) do |section, dependencies|
|
|
577
|
+
dependencies.merge!(package_json.fetch(section, {}))
|
|
578
|
+
end
|
|
579
|
+
rescue JSON::ParserError
|
|
580
|
+
{}
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def package_json_scripts
|
|
584
|
+
scripts = parsed_package_json.fetch("scripts", {})
|
|
585
|
+
scripts.is_a?(Hash) ? scripts : {}
|
|
586
|
+
rescue JSON::ParserError
|
|
587
|
+
{}
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def parsed_package_json
|
|
591
|
+
@package_json ||= begin
|
|
592
|
+
package_json_file? ? JSON.parse(File.read(package_json_path)) : {}
|
|
593
|
+
end
|
|
594
|
+
rescue JSON::ParserError
|
|
595
|
+
{}
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
def package_json_file?
|
|
599
|
+
File.exist?(package_json_path)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def package_json_path
|
|
603
|
+
File.join(destination_root, "package.json")
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def frontend_install_command(packages)
|
|
607
|
+
"#{package_manager_add_command} #{packages.join(" ")}"
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def package_manager_add_command
|
|
611
|
+
return "pnpm add" if File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
|
|
612
|
+
return "yarn add" if File.exist?(File.join(destination_root, "yarn.lock"))
|
|
613
|
+
return "bun add" if File.exist?(File.join(destination_root, "bun.lock")) ||
|
|
614
|
+
File.exist?(File.join(destination_root, "bun.lockb"))
|
|
615
|
+
|
|
616
|
+
"npm install"
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def package_manager_add_dev_command
|
|
620
|
+
return "pnpm add -D" if File.exist?(File.join(destination_root, "pnpm-lock.yaml"))
|
|
621
|
+
return "yarn add -D" if File.exist?(File.join(destination_root, "yarn.lock"))
|
|
622
|
+
return "bun add -d" if File.exist?(File.join(destination_root, "bun.lock")) ||
|
|
623
|
+
File.exist?(File.join(destination_root, "bun.lockb"))
|
|
624
|
+
|
|
625
|
+
"npm install --save-dev"
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def tailwind_build_pipeline?
|
|
629
|
+
tailwind_package_script? ||
|
|
630
|
+
tailwind_rails_gem_supported?
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def tailwind_cli_package?
|
|
634
|
+
package_json_dependencies.key?(TAILWIND_CLI_PACKAGE_NAME)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def tailwind_cli_supported?
|
|
638
|
+
requirement = package_json_dependencies[TAILWIND_CLI_PACKAGE_NAME]
|
|
639
|
+
requirement.present? && dependency_requirement_satisfies_minimum?(requirement, TAILWIND_MINIMUM_VERSION)
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def tailwind_package_script?
|
|
643
|
+
package_json_scripts.any? do |_name, command|
|
|
644
|
+
command = command.to_s
|
|
645
|
+
command.include?("tailwindcss") && command.include?(admin_stylesheet_path)
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def tailwind_build_script_name
|
|
650
|
+
"build:#{namespace_name}:css"
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def tailwind_build_command
|
|
654
|
+
"tailwindcss -i #{admin_stylesheet_path} -o app/assets/builds/#{namespace_name}.css --minify"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def tailwind_vite_plugin_package?
|
|
658
|
+
dependencies = package_json_dependencies
|
|
659
|
+
dependencies.key?(TAILWIND_VITE_PACKAGE_NAME)
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def tailwind_vite_plugin_supported?
|
|
663
|
+
requirement = package_json_dependencies[TAILWIND_VITE_PACKAGE_NAME]
|
|
664
|
+
requirement.present? && dependency_requirement_satisfies_minimum?(requirement, TAILWIND_MINIMUM_VERSION)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def admin_stylesheet_path
|
|
668
|
+
"app/assets/stylesheets/#{namespace_name}.css"
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def tailwind_rails_gem?
|
|
672
|
+
tailwind_rails_gem_requirement.present?
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def tailwind_rails_gem_supported?
|
|
676
|
+
requirement = tailwind_rails_gem_requirement
|
|
677
|
+
requirement.present? && dependency_requirement_satisfies_minimum?(requirement, TAILWIND_MINIMUM_VERSION)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def tailwind_rails_gem_requirement
|
|
681
|
+
locked_gem_version(TAILWIND_RAILS_GEM_NAME) ||
|
|
682
|
+
gemfile_dependency_requirement(TAILWIND_RAILS_GEM_NAME)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def locked_gem_version(name)
|
|
686
|
+
path = File.join(destination_root, "Gemfile.lock")
|
|
687
|
+
return nil unless File.exist?(path)
|
|
688
|
+
|
|
689
|
+
match = File.read(path).match(/^\s{4}#{Regexp.escape(name)} \((?<version>[^)\s]+).*\)$/)
|
|
690
|
+
match && match[:version]
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def gemfile_dependency_requirement(name)
|
|
694
|
+
path = File.join(destination_root, "Gemfile")
|
|
695
|
+
return nil unless File.exist?(path)
|
|
696
|
+
|
|
697
|
+
line = File.readlines(path).find { |candidate| candidate.match?(/^\s*gem\s+["']#{Regexp.escape(name)}["']/) }
|
|
698
|
+
return nil unless line
|
|
699
|
+
|
|
700
|
+
line.scan(/["']([^"']+)["']/).flatten[1] || "*"
|
|
701
|
+
end
|
|
702
|
+
|
|
82
703
|
def application_models
|
|
83
704
|
models_path = Rails.root.join("app", "models")
|
|
84
705
|
return [] unless models_path.exist?
|
|
@@ -91,6 +712,22 @@ module Terrazzo
|
|
|
91
712
|
relative.delete_suffix(".rb").camelize.safe_constantize
|
|
92
713
|
end.select { |klass| klass < ApplicationRecord && !klass.abstract_class? }
|
|
93
714
|
end
|
|
715
|
+
|
|
716
|
+
def table_exists_for?(model)
|
|
717
|
+
model.table_exists?
|
|
718
|
+
rescue ActiveRecord::ConnectionNotEstablished,
|
|
719
|
+
ActiveRecord::NoDatabaseError,
|
|
720
|
+
ActiveRecord::StatementInvalid
|
|
721
|
+
false
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def raise_no_models_error
|
|
725
|
+
raise Thor::Error, <<~MESSAGE
|
|
726
|
+
Terrazzo could not find any ApplicationRecord models to generate dashboards.
|
|
727
|
+
|
|
728
|
+
Add at least one model first, then run `bin/rails generate terrazzo:install` again.
|
|
729
|
+
MESSAGE
|
|
730
|
+
end
|
|
94
731
|
end
|
|
95
732
|
end
|
|
96
733
|
end
|