Class: Ruact::Generators::InstallGenerator

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

Overview

Installs ruact into the current Rails application.

Performs the following actions:

  1. Creates config/initializers/ruact.rb
  2. Injects include Ruact::Controller into ApplicationController
  3. Injects the React root div into app/views/layouts/application.html.erb
  4. Creates app/javascript/components/.keep
  5. Creates vite.config.js (or shows manual instructions if one exists)
  6. Creates package.json (react/react-dom/vite/@vitejs/plugin-react) so a fresh app has JS deps to install (Story 14.6, FR101).
  7. Creates Procfile.dev + bin/dev (foreman) so the single bin/dev command boots BOTH Rails and the Vite dev server (Story 14.6, Epic 14 DoD — a ruact app needs both processes).
  8. Runs npm install so JavaScript dependencies are ready (FR101); skippable via --skip-npm.

Story 14.2 (FR104) — the generator no longer writes a bootstrap entry into the user's tree. ruact's React entry is served as the virtual module virtual:ruact/bootstrap by the bundled Vite plugin (the generated vite.config input points at it), so a fresh install leaves app/javascript/ with only the user's components/ (plus the gitignored, typed .ruact/server-functions.ts).

Run: rails generate ruact:install

Instance Method Summary collapse

Instance Method Details

#advise_plumbing_migrationObject

Story 14.2 (FR104, AC7) — an app upgrading from the earlier layout still has app/javascript/Ruact::Generators::InstallGenerator.applicationapplication.jsx,flight-clientapplication.jsx,flight-client.js,ruact-routerapplication.jsx,flight-client.js,ruact-router.js on disk. The generator never deletes user files, so it prints the exact manual steps to reach the hidden-plumbing layout — otherwise the app is left half-wired, with the stale entry shadowing the virtual bootstrap. No-op on a fresh install (none of those files exist).



190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/generators/ruact/install/install_generator.rb', line 190

def advise_plumbing_migration
  stale = legacy_plumbing_files
  return if stale.empty?

  say_status "notice", "earlier ruact layout detected — finish the Story 14.2 migration:", :yellow
  say ""
  say "  ruact's bootstrap entry + Flight runtime are now hidden behind the"
  say "  virtual module '#{Ruact.bootstrap_virtual_id}' (served from the gem)."
  say "  Remove these now-obsolete files so they don't shadow the virtual entry:"
  stale.each { |f| say "    - delete #{f}" }
  say ""
  say "  Then set your vite.config build input to '#{Ruact.bootstrap_virtual_id}'"
  say "  (or re-run with --force to regenerate vite.config.js), and let the"
  say "  controller's HTML shell — or the `ruact_js_assets` view helper in your"
  say "  layout — emit the entry <script> tags."
  say ""
end

#append_gitignore_entriesObject



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

def append_gitignore_entries
  gitignore = Pathname(destination_root).join(".gitignore")
  return unless gitignore.exist?

  entries = [
    "app/javascript/.ruact/server-functions.ts",
    "tmp/cache/ruact/"
  ]
  # Substring matches (`existing.include?(entry)`) were unsafe — they
  # would skip "tmp/cache/ruact/" when the file already contained
  # "tmp/cache/ruact/some-cache.bin", leaving the directory itself
  # un-ignored. Match by exact normalized line instead.
  existing_lines = File.read(gitignore).each_line.to_set { |line| line.chomp.strip }
  new_entries = entries.reject { |e| existing_lines.include?(e) }
  return if new_entries.empty?

  append_to_file ".gitignore", "\n# ruact (Story 8.0a — auto-generated server-functions module)\n"
  new_entries.each { |entry| append_to_file ".gitignore", "#{entry}\n" }
end

#create_components_directoryObject



81
82
83
84
85
# File 'lib/generators/ruact/install/install_generator.rb', line 81

def create_components_directory
  empty_directory "app/javascript/components"
  create_file "app/javascript/components/.keep" unless
    File.exist?(Pathname(destination_root).join("app/javascript/components/.keep"))
end

#create_initializerObject



47
48
49
# File 'lib/generators/ruact/install/install_generator.rb', line 47

def create_initializer
  template "initializer.rb.tt", "config/initializers/ruact.rb"
end

#create_launch_filesObject

Story 14.6 (Epic 14 DoD) — emit Procfile.dev + a foreman-based bin/dev so the literal bin/dev boots BOTH processes a ruact app needs: Rails (HTML shell + Flight + server functions) and the Vite dev server (React/HMR + the bundled ruact plugin). Without these, bin/dev would start only Rails and the React assets would never be served.

Procfile.dev is guarded (skip if present unless --force) — the app may already drive its own processes through one. bin/dev, however, is OWNED by ruact: see install_foreman_launcher. bin/dev is made executable.



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

def create_launch_files
  create_guarded_file "Procfile.dev", "Procfile.dev.tt"
  install_foreman_launcher
  # Ensure bin/dev is executable whether we just wrote it or it pre-existed
  # (a skipped, already-foreman launcher should still be runnable).
  chmod "bin/dev", 0o755, verbose: false if Pathname(destination_root).join("bin/dev").exist?
end

#create_package_jsonObject

Story 14.6 (FR101, Epic 14 DoD) — a fresh ruact app needs a package.json declaring its JavaScript dependencies (React + Vite + the React Vite plugin) so the npm install that Story 14.1 runs has something to resolve and bin/dev's npm run dev (Vite) has a dev script. The bundled ruact Vite plugin is NOT a package.json dependency — vite.config imports it by the absolute Ruact.vite_plugin_path and it uses only node: builtins. Guarded like vite.config.js: an existing package.json is left untouched (the app may already have one) unless --force.



154
155
156
157
158
159
160
161
162
163
164
# File 'lib/generators/ruact/install/install_generator.rb', line 154

def create_package_json
  package_json_file = Pathname(destination_root).join("package.json")

  if package_json_file.exist? && !options[:force]
    say_status "skip", "package.json already exists — ensure it has react, react-dom, " \
                       "vite and @vitejs/plugin-react (re-run with --force to overwrite)", :yellow
    return
  end

  template "package.json.tt", "package.json"
end

#create_server_functions_directoryObject

Story 8.0a — scaffold the directory the codegen writes into and add the generated artifacts to .gitignore. The TS module is regenerated on every boot from the action and query registries, so it should never be version-controlled; same for the bridge JSON under tmp/cache/.



91
92
93
94
95
# File 'lib/generators/ruact/install/install_generator.rb', line 91

def create_server_functions_directory
  empty_directory "app/javascript/.ruact"
  create_file "app/javascript/.ruact/.gitkeep" unless
    File.exist?(Pathname(destination_root).join("app/javascript/.ruact/.gitkeep"))
end

#create_vite_configObject



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/generators/ruact/install/install_generator.rb', line 127

def create_vite_config
  vite_config_file = Pathname(destination_root).join("vite.config.js")

  # `--force` must actually regenerate (the message below promises it). The
  # guard skips the template ONLY when the file exists AND force was not
  # passed; with --force we fall through to `template`, which overwrites.
  if vite_config_file.exist? && !options[:force]
    say_status "notice", "vite.config.js already exists — add the plugin manually:", :yellow
    say "  1. At the top of vite.config.js, add:"
    say "       import ruact from '#{Ruact.vite_plugin_path}';"
    say "  2. In the plugins array, add: ruact()"
    say "  3. Set build.rollupOptions.input to '#{Ruact.bootstrap_virtual_id}'"
    say ""
    say "  Re-run `rails generate ruact:install --force` to overwrite vite.config.js."
  else
    template "vite.config.js.tt", "vite.config.js"
  end
end

#inject_controller_concernObject



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/generators/ruact/install/install_generator.rb', line 51

def inject_controller_concern
  controller_file = "app/controllers/application_controller.rb"
  return unless File.exist?(Pathname(destination_root).join(controller_file))

  content = File.read(Pathname(destination_root).join(controller_file))
  if content.include?("Ruact::Controller")
    say_status "skip", "Ruact::Controller already included in ApplicationController", :yellow
    return
  end

  inject_into_file controller_file,
                   "\n  include Ruact::Controller\n",
                   after: /class ApplicationController.*\n/
end

#inject_layout_shellObject



66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/generators/ruact/install/install_generator.rb', line 66

def inject_layout_shell
  layout_file = "app/views/layouts/application.html.erb"
  return unless File.exist?(Pathname(destination_root).join(layout_file))

  content = File.read(Pathname(destination_root).join(layout_file))
  if content.include?("ruact: root")
    say_status "skip", "Rails RSC root already present in layout", :yellow
    return
  end

  inject_into_file layout_file,
                   "\n    <%# ruact: root %>\n    <div id=\"root\"></div>\n",
                   before: "  </body>"
end

#install_javascript_dependenciesObject

Story 14.1 (FR101) — install JavaScript dependencies so a fresh app is runnable in one command. Runs LAST among the file-producing actions (after every file is written) so a failure here leaves the generated files in place and reported. Records the outcome in @npm_outcome so show_post_install_message can tell the truth about what happened.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/generators/ruact/install/install_generator.rb', line 213

def install_javascript_dependencies
  if options[:skip_npm]
    @npm_outcome = :skipped
    say_status "skip", "npm install (--skip-npm) — install JS deps manually before bin/dev", :yellow
    return
  end

  unless npm_on_path?
    @npm_outcome = :unavailable
    warn_npm_unavailable
    return
  end

  # Thor's `run` returns false on a non-zero exit (Rails generators set
  # exit_on_failure? = false, so a failed `npm install` does NOT raise),
  # and nil under `--pretend` (the command is previewed, not executed).
  # Branch so the post-install message never claims deps are installed
  # when npm failed (AC#3) — and a `--pretend` dry run is NOT misreported
  # as a failure (run_npm_install still prints the previewed command).
  if run_npm_install || options[:pretend]
    @npm_outcome = options[:pretend] ? :pretend : :installed
  else
    @npm_outcome = :failed
    warn_npm_install_failed
  end
end

#prime_server_functions_codegenObject

Invokes ruact:server_functions:generate so a fresh install completes with the AC8-required empty-but-valid generated module on disk. Failures (a NameBridge violation, a collision, an unwritable tmp/cache/ruact/ directory) propagate intentionally — silencing them via a rescue would let an install finish in a broken state, which is the bug the Re-run review caught.



123
124
125
# File 'lib/generators/ruact/install/install_generator.rb', line 123

def prime_server_functions_codegen
  rake "ruact:server_functions:generate"
end

#show_post_install_messageObject



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

def show_post_install_message
  say "\n#{'=' * 60}\n  ruact installed successfully!\n#{'=' * 60}\n"

  if @npm_outcome == :installed
    say "JavaScript dependencies are installed."
    say ""
    say "Next step:"
    say "  Start your app:  bin/dev"
  else
    say "JavaScript dependencies are not yet installed."
    say ""
    say "Next steps:"
    say "  1. Install JS dependencies:  npm install"
    say "  2. Start your app:           bin/dev"
  end

  say "\nThen add <MyComponent /> to any ERB view.\n"
  say "Note: re-run this generator after updating the ruact gem to refresh"
  say "the bundled Vite plugin path in vite.config.js."
  say ""
end