Class: Terrazzo::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Defined in:
lib/generators/terrazzo/install/install_generator.rb

Constant Summary collapse

TAILWIND_PACKAGE_NAME =
"tailwindcss"
TAILWIND_CLI_PACKAGE_NAME =
"@tailwindcss/cli"
TAILWIND_VITE_PACKAGE_NAME =
"@tailwindcss/vite"
TAILWIND_RAILS_GEM_NAME =
"tailwindcss-rails"
TAILWIND_MINIMUM_VERSION =
"4.0.0"
FRONTEND_DEPENDENCIES =
(
  %w[
    terrazzo
    react
    react-dom
    react-redux
    @reduxjs/toolkit
    @thoughtbot/superglue
    @radix-ui/react-avatar
    @radix-ui/react-dialog
    @radix-ui/react-dropdown-menu
    @radix-ui/react-label
    @radix-ui/react-popover
    @radix-ui/react-select
    @radix-ui/react-separator
    @radix-ui/react-slot
    @radix-ui/react-tooltip
    class-variance-authority
    lucide-react
  ] + [TAILWIND_PACKAGE_NAME]
).freeze
FRONTEND_DEPENDENCY_MINIMUMS =
{
  "terrazzo" => Terrazzo::VERSION,
  "react" => "18.0.0",
  "react-dom" => "18.0.0",
  "react-redux" => "8.0.0",
  "@reduxjs/toolkit" => "1.9.0",
  "@thoughtbot/superglue" => "1.0.0",
  "@radix-ui/react-avatar" => "1.0.0",
  "@radix-ui/react-dialog" => "1.0.0",
  "@radix-ui/react-dropdown-menu" => "2.0.0",
  "@radix-ui/react-label" => "2.0.0",
  "@radix-ui/react-popover" => "1.0.0",
  "@radix-ui/react-select" => "2.0.0",
  "@radix-ui/react-separator" => "1.0.0",
  "@radix-ui/react-slot" => "1.0.0",
  "@radix-ui/react-tooltip" => "1.0.0",
  "class-variance-authority" => "0.7.0",
  "lucide-react" => "0.300.0",
  TAILWIND_PACKAGE_NAME => TAILWIND_MINIMUM_VERSION,
}.freeze
VITE_FRONTEND_DEPENDENCIES =
%w[
  vite
  vite-plugin-ruby
].freeze
SUPPORTED_BUNDLERS =
%w[vite esbuild].freeze

Instance Method Summary collapse

Instance Method Details

#configure_vite_dev_serverObject



176
177
178
179
180
181
# File 'lib/generators/terrazzo/install/install_generator.rb', line 176

def configure_vite_dev_server
  return unless vite?

  ensure_vite_procfile
  ensure_vite_bin_dev
end

#configure_vite_watch_pathsObject



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/generators/terrazzo/install/install_generator.rb', line 158

def configure_vite_watch_paths
  return unless vite?

  config = parsed_vite_config
  return unless config

  watch_paths = vite_watch_paths
  return if watch_paths.empty?

  config["all"] = {} unless config["all"].is_a?(Hash)
  existing_paths = Array(config["all"]["watchAdditionalPaths"])
  updated_paths = existing_paths | watch_paths
  return if updated_paths == existing_paths

  config["all"]["watchAdditionalPaths"] = updated_paths
  create_file "config/vite.json", "#{JSON.pretty_generate(config)}\n", force: true
end

#create_application_controllerObject



131
132
133
134
# File 'lib/generators/terrazzo/install/install_generator.rb', line 131

def create_application_controller
  template "application_controller.rb.erb",
    "app/controllers/#{namespace_name}/application_controller.rb"
end

#create_application_visitObject



210
211
212
213
# File 'lib/generators/terrazzo/install/install_generator.rb', line 210

def create_application_visit
  template "application_visit.js.erb",
    "app/javascript/#{namespace_name}/application_visit.js"
end

#create_components_jsonObject



262
263
264
265
266
267
268
269
# File 'lib/generators/terrazzo/install/install_generator.rb', line 262

def create_components_json
  if File.exist?(File.join(destination_root, "components.json"))
    say_status :skip, "components.json already exists", :yellow
    return
  end

  template "components.json.erb", "components.json"
end

#create_custom_page_mappingObject



205
206
207
208
# File 'lib/generators/terrazzo/install/install_generator.rb', line 205

def create_custom_page_mapping
  template "custom_page_mapping.js.erb",
    "app/javascript/#{namespace_name}/custom_page_mapping.js"
end

#create_esbuild_entry_pointObject



183
184
185
186
187
188
# File 'lib/generators/terrazzo/install/install_generator.rb', line 183

def create_esbuild_entry_point
  return unless esbuild?

  create_file "app/javascript/#{namespace_name}.js",
    %(import "./#{namespace_name}/application.jsx"\n)
end

#create_flash_sliceObject



215
216
217
218
# File 'lib/generators/terrazzo/install/install_generator.rb', line 215

def create_flash_slice
  template "flash_slice.js.erb",
    "app/javascript/#{namespace_name}/slices/flash.js"
end

#create_generated_page_mappingObject



200
201
202
203
# File 'lib/generators/terrazzo/install/install_generator.rb', line 200

def create_generated_page_mapping
  template "generated_page_mapping.js.erb",
    "app/javascript/#{namespace_name}/generated_page_mapping.js"
end

#create_js_entry_pointObject



146
147
148
# File 'lib/generators/terrazzo/install/install_generator.rb', line 146

def create_js_entry_point
  template "application.js.erb", js_entry_point_path
end

#create_jsconfigObject



271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/generators/terrazzo/install/install_generator.rb', line 271

def create_jsconfig
  if File.exist?(File.join(destination_root, "jsconfig.json"))
    say_status :skip, "jsconfig.json already exists", :yellow
    return
  end

  if File.exist?(File.join(destination_root, "tsconfig.json"))
    say_status :skip, "tsconfig.json already exists", :yellow
    return
  end

  copy_file "jsconfig.json", "jsconfig.json"
end

#create_json_props_layoutObject



141
142
143
144
# File 'lib/generators/terrazzo/install/install_generator.rb', line 141

def create_json_props_layout
  template "application.json.props.tt",
    "app/views/layouts/#{namespace_name}/application.json.props"
end

#create_layoutObject



136
137
138
139
# File 'lib/generators/terrazzo/install/install_generator.rb', line 136

def create_layout
  template "superglue.html.erb.erb",
    "app/views/layouts/#{namespace_name}/application.html.erb"
end

#create_page_to_page_mappingObject



195
196
197
198
# File 'lib/generators/terrazzo/install/install_generator.rb', line 195

def create_page_to_page_mapping
  template "page_to_page_mapping.js.erb",
    "app/javascript/#{namespace_name}/page_to_page_mapping.js"
end

#create_storeObject



190
191
192
193
# File 'lib/generators/terrazzo/install/install_generator.rb', line 190

def create_store
  template "store.js.erb",
    "app/javascript/#{namespace_name}/store.js"
end

#create_stylesheetObject



220
221
222
223
# File 'lib/generators/terrazzo/install/install_generator.rb', line 220

def create_stylesheet
  copy_file "admin.css",
    "app/assets/stylesheets/#{namespace_name}.css"
end

#create_tailwind_build_scriptObject



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/generators/terrazzo/install/install_generator.rb', line 225

def create_tailwind_build_script
  return unless package_json_file?
  return if tailwind_build_pipeline?
  return unless tailwind_cli_package?
  return unless tailwind_cli_supported?

  package_json = parsed_package_json
  return if package_json.empty?

  scripts = package_json["scripts"]
  scripts = package_json["scripts"] = {} unless scripts.is_a?(Hash)
  return if scripts.key?(tailwind_build_script_name)

  scripts[tailwind_build_script_name] = tailwind_build_command
  create_file "package.json", "#{JSON.pretty_generate(package_json)}\n", force: true
end

#create_vite_build_scriptObject



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/generators/terrazzo/install/install_generator.rb', line 242

def create_vite_build_script
  return unless vite?
  return unless package_json_file?

  package_json = parsed_package_json
  return if package_json.empty?

  scripts = package_json["scripts"]
  scripts = package_json["scripts"] = {} unless scripts.is_a?(Hash)
  return if scripts.key?("build")

  build_commands = ["vite build"]
  if scripts[tailwind_build_script_name] == tailwind_build_command
    build_commands << tailwind_build_command
  end

  scripts["build"] = build_commands.join(" && ")
  create_file "package.json", "#{JSON.pretty_generate(package_json)}\n", force: true
end

#create_vite_entry_pointObject



150
151
152
153
154
155
156
# File 'lib/generators/terrazzo/install/install_generator.rb', line 150

def create_vite_entry_point
  return unless vite?
  return if vite_entry_point_path == js_entry_point_path

  create_file vite_entry_point_path,
    %(import "#{relative_import_path(vite_entry_point_directory, js_entry_point_path)}"\n)
end

#run_dashboard_generatorsObject



293
294
295
296
297
# File 'lib/generators/terrazzo/install/install_generator.rb', line 293

def run_dashboard_generators
  application_models.each do |model|
    generate "terrazzo:dashboard", model.name, "--namespace=#{namespace_name}", "--bundler=#{options[:bundler]}"
  end
end

#run_routes_generatorObject



289
290
291
# File 'lib/generators/terrazzo/install/install_generator.rb', line 289

def run_routes_generator
  generate "terrazzo:routes", "--namespace=#{namespace_name}"
end

#run_views_generatorObject



285
286
287
# File 'lib/generators/terrazzo/install/install_generator.rb', line 285

def run_views_generator
  generate "terrazzo:views", "--namespace=#{namespace_name}"
end

#validate_bundlerObject

Raises:

  • (Thor::Error)


72
73
74
75
76
77
78
79
80
81
# File 'lib/generators/terrazzo/install/install_generator.rb', line 72

def validate_bundler
  return if SUPPORTED_BUNDLERS.include?(options[:bundler])

  raise Thor::Error, <<~MESSAGE
    Unsupported bundler '#{options[:bundler]}'.

    Terrazzo generates React/JSX admin views and supports Vite or esbuild.
    Re-run with `--bundler=vite` or `--bundler=esbuild`.
  MESSAGE
end

#validate_namespaceObject

Raises:

  • (Thor::Error)


83
84
85
86
87
88
89
90
91
92
93
# File 'lib/generators/terrazzo/install/install_generator.rb', line 83

def validate_namespace
  return if namespace_argument.blank?
  return if options[:namespace] == "admin" || options[:namespace] == namespace_argument

  raise Thor::Error, <<~MESSAGE
    Conflicting admin namespaces: '#{namespace_argument}' and '#{options[:namespace]}'.

    Use either `rails generate terrazzo:install #{namespace_argument}` or
    `rails generate terrazzo:install --namespace=#{options[:namespace]}`, not both.
  MESSAGE
end

#verify_database_schemaObject

Raises:

  • (Thor::Error)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/generators/terrazzo/install/install_generator.rb', line 116

def verify_database_schema
  models = application_models
  raise_no_models_error if models.empty?

  missing_tables = models.reject { |model| table_exists_for?(model) }
  return if missing_tables.empty?

  model_names = missing_tables.map(&:name).join(", ")
  raise Thor::Error, <<~MESSAGE
    Terrazzo could not generate dashboards because these models do not have database tables: #{model_names}

    Run `bin/rails db:prepare` first, then run `bin/rails generate terrazzo:install` again.
  MESSAGE
end

#verify_frontend_dependenciesObject



299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/generators/terrazzo/install/install_generator.rb', line 299

def verify_frontend_dependencies
  missing = missing_frontend_dependencies
  outdated = outdated_frontend_dependencies

  if missing.any?
    say_status :warning, "Missing frontend dependencies required by Terrazzo:", :yellow
    say "  #{missing.join(" ")}"
    say "Run: #{frontend_install_command(missing)}"
  end

  return if outdated.empty?

  say_status :warning, "Frontend dependencies below Terrazzo's supported versions:", :yellow
  outdated.each do |dependency|
    say "  #{dependency[:name]} #{dependency[:requirement]} (requires >= #{dependency[:minimum]})"
  end
  say "Run: #{frontend_install_command(outdated.map { |dependency| "#{dependency[:name]}@latest" })}"
end

#verify_tailwind_build_pipelineObject



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/generators/terrazzo/install/install_generator.rb', line 318

def verify_tailwind_build_pipeline
  if tailwind_cli_package? && !tailwind_cli_supported?
    say_status :warning, "Installed #{TAILWIND_CLI_PACKAGE_NAME} is below Terrazzo's supported version.", :yellow
    say "Terrazzo generated app/assets/stylesheets/#{namespace_name}.css with Tailwind #{TAILWIND_MINIMUM_VERSION}+ syntax."
    say "Run: #{package_manager_add_dev_command} #{TAILWIND_CLI_PACKAGE_NAME}@latest"
    return
  end

  if tailwind_rails_gem? && !tailwind_rails_gem_supported?
    say_status :warning, "Installed #{TAILWIND_RAILS_GEM_NAME} is below Terrazzo's supported version.", :yellow
    say "Terrazzo generated app/assets/stylesheets/#{namespace_name}.css with Tailwind #{TAILWIND_MINIMUM_VERSION}+ syntax."
    say "Update #{TAILWIND_RAILS_GEM_NAME} to #{TAILWIND_MINIMUM_VERSION} or newer, or install #{TAILWIND_CLI_PACKAGE_NAME}@latest."
    return
  end

  return if tailwind_build_pipeline?

  say_status :warning, "Terrazzo generated app/assets/stylesheets/#{namespace_name}.css but no Tailwind build pipeline was detected.", :yellow
  say "Make sure your app compiles that file with Tailwind before serving the admin UI."
  if tailwind_vite_plugin_package?
    say "#{TAILWIND_VITE_PACKAGE_NAME} is installed, but Terrazzo links #{admin_stylesheet_path} as a Rails stylesheet asset."
  end
  say "For package.json scripts, install the Tailwind CLI and add a build script:"
  say "  #{package_manager_add_dev_command} #{TAILWIND_CLI_PACKAGE_NAME}@latest"
  say %(  "#{tailwind_build_script_name}": "#{tailwind_build_command}")
end

#verify_vite_bundlerObject

Raises:

  • (Thor::Error)


95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/generators/terrazzo/install/install_generator.rb', line 95

def verify_vite_bundler
  return unless vite?

  unless vite_rails_installed? && parsed_vite_config
    raise Thor::Error, <<~MESSAGE
      Terrazzo is configured for Vite, but vite_rails is not installed and configured.

      Add `gem "vite_rails"` and run `bundle exec vite install` first, or re-run Terrazzo with `--bundler=esbuild`.
    MESSAGE
  end

  missing = missing_vite_frontend_dependencies
  return if missing.empty?

  raise Thor::Error, <<~MESSAGE
    Terrazzo is configured for Vite, but package.json is missing required Vite dependencies: #{missing.join(", ")}.

    Run `bundle exec vite install` successfully, install the generated package.json dependencies, or re-run Terrazzo with `--bundler=esbuild`.
  MESSAGE
end