Class: Ruact::Generators::InstallGenerator
- Inherits:
-
Rails::Generators::Base
- Object
- Rails::Generators::Base
- Ruact::Generators::InstallGenerator
- Defined in:
- lib/generators/ruact/install/install_generator.rb
Overview
Installs ruact into the current Rails application.
Performs the following actions:
- Creates config/initializers/ruact.rb
- Injects
include Ruact::Controllerinto ApplicationController - Injects the React root div AND
ruact_js_assetsinto app/views/layouts/application.html.erb, so the app's own layout owns the document (and its<head>— stylesheets, fonts, meta — reaches a ruact page). See Ruact::Configuration#layout. - Creates app/javascript/components/.keep
- Creates vite.config.js (or shows manual instructions if one exists)
- Creates package.json (react/react-dom/vite/@vitejs/plugin-react) so a fresh app has JS deps to install (Story 14.6, FR101).
- Creates Procfile.dev + bin/dev (foreman) so the single
bin/devcommand boots BOTH Rails and the Vite dev server (Story 14.6, Epic 14 DoD — a ruact app needs both processes). - Emits AGENTS.md (marker-delimited, append-aware, idempotent) so coding agents working in the app have ruact's conventions, traps, and verification commands in context by default (Story 15.1, FR105).
- Runs
npm installso JavaScript dependencies are ready (FR101); skippable via --skip-npm. - With
--shadcn: also emits the prerequisites shadcn's own CLI refuses to initialize without (a Tailwind entry + atsconfig.jsonimport alias), wires thecssbuild process, and prints the twonpx shadcncommands it deliberately does not run.
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
-
#advise_plumbing_migration ⇒ Object
Story 14.2 (FR104, AC7) — an app upgrading from the earlier layout still has app/javascript/applicationapplication.jsx,flight-clientapplication.jsx,flight-client.js,ruact-routerapplication.jsx,flight-client.js,ruact-router.js on disk.
- #append_gitignore_entries ⇒ Object
-
#create_agents_md ⇒ Object
Story 15.1 (FR105) — emit AGENTS.md so coding agents working in the app have ruact's conventions, traps, and verification commands in context by default.
- #create_components_directory ⇒ Object
-
#create_initializer ⇒ Object
A fresh app gets the whole initializer.
-
#create_launch_files ⇒ Object
Story 14.6 (Epic 14 DoD) — emit
Procfile.dev+ a foreman-basedbin/devso the literalbin/devboots BOTH processes a ruact app needs: Rails (HTML shell + Flight + server functions) and the Vite dev server (React/HMR + the bundled ruact plugin). -
#create_package_json ⇒ Object
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 installthat Story 14.1 runs has something to resolve andbin/dev'snpm run dev(Vite) has adevscript. -
#create_server_functions_directory ⇒ Object
Story 8.0a — scaffold the directory the codegen writes into and add the generated artifacts to .gitignore.
-
#create_shadcn_prerequisites ⇒ Object
--shadcnonly. - #create_vite_config ⇒ Object
- #inject_controller_concern ⇒ Object
-
#inject_layout_shell ⇒ Object
The layout owns the document:
stylesheet_link_tag, favicons, fonts and every<head>-writing gem only reach a ruact page because Rails' own layout renders it (seeRuact::Configuration#layout). -
#install_javascript_dependencies ⇒ Object
Story 14.1 (FR101) — install JavaScript dependencies so a fresh app is runnable in one command.
-
#prime_server_functions_codegen ⇒ Object
Invokes
ruact:server_functions:generateso a fresh install completes with the AC8-required empty-but-valid generated module on disk. - #show_post_install_message ⇒ Object
Instance Method Details
#advise_plumbing_migration ⇒ Object
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).
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
# File 'lib/generators/ruact/install/install_generator.rb', line 301 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_entries ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/generators/ruact/install/install_generator.rb', line 204 def append_gitignore_entries gitignore = Pathname(destination_root).join(".gitignore") return unless gitignore.exist? entries = [ "app/javascript/.ruact/server-functions.ts", "tmp/cache/ruact/" ] # The compiled stylesheet is a build artifact of globals.css, rebuilt by # the Procfile's `css` process on every boot — same reasoning as the # generated server-functions module above. entries << "app/assets/builds/tailwind.css" if shadcn? # 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_agents_md ⇒ Object
Story 15.1 (FR105) — emit AGENTS.md so coding agents working in the app
have ruact's conventions, traps, and verification commands in context
by default. The ruact content is delimited by explicit markers
(<!-- ruact:begin --> / <!-- ruact:end -->) so the action can be
append-aware and idempotent:
no file → create it (the template IS the section)
file without markers → APPEND the marked section, every
pre-existing user byte preserved
markers present → skip (re-running install is zero-diff)
markers present + --force → refresh ONLY the between-marker content —
deliberately narrower than the vite.config
full-overwrite posture, because a user's
AGENTS.md may carry their own project
instructions above/below ruact's section.
Later stories (15.2 loud children error, 15.3 --json introspection,
15.4 test helpers) evolve the template; --force after a gem upgrade
is the designed refresh path.
338 339 340 341 342 343 344 345 346 347 348 349 350 351 |
# File 'lib/generators/ruact/install/install_generator.rb', line 338 def create_agents_md destination = Pathname(destination_root).join("AGENTS.md") return template("AGENTS.md.tt", "AGENTS.md") unless destination.exist? content = destination.read if agents_md_markers_well_formed?(content) refresh_or_skip_agents_md_section elsif agents_md_markers_broken?(content) warn_agents_md_broken_markers else append_agents_md_section(content) end end |
#create_components_directory ⇒ Object
188 189 190 191 192 |
# File 'lib/generators/ruact/install/install_generator.rb', line 188 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_initializer ⇒ Object
A fresh app gets the whole initializer. An app that ALREADY has one gets
a surgical injection instead of template's overwrite prompt, which
offered a bad choice on the one path that matters most — migrating an
existing app: overwrite and lose every setting the app had
(strict_serialization, manifest_path, the SGID defaults…), or skip
and end up half-migrated, with the layout edited but config.layout
still off and nothing saying so except ruact:doctor.
79 80 81 82 83 84 |
# File 'lib/generators/ruact/install/install_generator.rb', line 79 def create_initializer path = Pathname(destination_root).join("config/initializers/ruact.rb") return template "initializer.rb.tt", "config/initializers/ruact.rb" unless path.exist? inject_layout_setting(path) end |
#create_launch_files ⇒ Object
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.
287 288 289 290 291 292 293 |
# File 'lib/generators/ruact/install/install_generator.rb', line 287 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_json ⇒ Object
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.
265 266 267 268 269 270 271 272 273 274 275 |
# File 'lib/generators/ruact/install/install_generator.rb', line 265 def create_package_json package_json_file = Pathname(destination_root).join("package.json") if package_json_file.exist? && ![: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_directory ⇒ Object
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/.
198 199 200 201 202 |
# File 'lib/generators/ruact/install/install_generator.rb', line 198 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_shadcn_prerequisites ⇒ Object
--shadcn only. Two files, both of them things shadcn's CLI checks for
and refuses to proceed without ("No Tailwind CSS configuration found" /
"Could not find valid path aliases"), verified against shadcn 4.x:
app/javascript/styles/globals.css — the Tailwind entry. shadcn appends
its design tokens here, which is why components.json points at it.
tsconfig.json — the `@/*` → `app/javascript/*` alias. The Vite plugin
already registers the same alias for the BUNDLER, so components
resolve at runtime today; this is what makes it resolve for
TypeScript (and therefore for shadcn's alias probe and your editor).
Both are guarded: an app that already has them keeps its own.
173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/generators/ruact/install/install_generator.rb', line 173 def create_shadcn_prerequisites return unless shadcn? create_guarded_file "app/javascript/styles/globals.css", "globals.css.tt" create_guarded_file "tsconfig.json", "tsconfig.json.tt" # Propshaft only serves directories that exist; the built stylesheet is # generated, so the directory ships with a .keep and the artifact is # gitignored (see append_gitignore_entries). empty_directory "app/assets/builds" create_file "app/assets/builds/.keep" unless File.exist?(Pathname(destination_root).join("app/assets/builds/.keep")) warn_unless_layout_links_builds end |
#create_vite_config ⇒ Object
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 |
# File 'lib/generators/ruact/install/install_generator.rb', line 238 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? && ![: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_concern ⇒ Object
86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/generators/ruact/install/install_generator.rb', line 86 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_shell ⇒ Object
The layout owns the document: stylesheet_link_tag, favicons, fonts and
every <head>-writing gem only reach a ruact page because Rails' own
layout renders it (see Ruact::Configuration#layout). That requires TWO
things in the layout — the React root, and ruact_js_assets to emit the
bootstrap entry + this render's Flight payload. The other half of the
opt-in is config.layout = true, which the generated initializer
carries — a layout with the helper but the setting off (or the reverse)
keeps rendering through ruact's built-in, CSS-less shell.
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/generators/ruact/install/install_generator.rb', line 109 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)) # A CALL, not a mention: `<%# TODO: add ruact_js_assets %>` used to read # as "already present" here and skip the migration, leaving the app on # ruact's CSS-less shell with the generator reporting success. Shared # with the runtime so both agree on what "migrated" means. # BOTH halves, not just the helper. A layout carrying `ruact_js_assets` # with no `<div id="root"></div>` was skipped as "already present" — and # since this generator also turns `config.layout` on, that app then # raised at render time on a document React could not mount into. if Ruact::LayoutSource.wired?(content) && Ruact::LayoutSource.root?(content) say_status "skip", "ruact root + assets already present in layout", :yellow return end # Migration path for an app installed before the layout owned the # document: the root is already there, only the asset call is missing. # # The anchor matches the ROOT DIV itself rather than the marker-then-div # pair, and tolerates the ways a real layout is written — single or # double quotes, extra attributes, any attribute order, CRLF, and the # marker on the same line. The earlier anchor required the exact emitted # formatting, so a hand-edited layout silently matched nothing. The # attribute boundary in `ROOT_ELEMENT` is what keeps `data-id="root"` # from being mistaken for the mount point. if Ruact::LayoutSource.root?(content) return migrate_layout(layout_file, "\n <%= ruact_js_assets %>", after: Ruact::LayoutSource::ROOT_ELEMENT, success: "added ruact_js_assets to the existing layout root") end # The mirror case: the helper is there but the mount target is not, so # the root goes in just BEFORE the call (React needs the node in the # document, and keeping the pair adjacent matches what a fresh install # writes). Injecting the whole block instead would duplicate the helper. if Ruact::LayoutSource.wired?(content) return migrate_layout(layout_file, "<%# ruact: root %>\n <div id=\"root\"></div>\n ", before: Ruact::LayoutSource::ASSETS_CALL, success: "added the React root div next to the existing ruact_js_assets") end inject_into_file layout_file, "\n <%# ruact: root %>\n <div id=\"root\"></div>\n <%= ruact_js_assets %>\n", before: " </body>" end |
#install_javascript_dependencies ⇒ Object
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.
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/generators/ruact/install/install_generator.rb', line 358 def install_javascript_dependencies if [: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 || [:pretend] @npm_outcome = [:pretend] ? :pretend : :installed else @npm_outcome = :failed warn_npm_install_failed end end |
#prime_server_functions_codegen ⇒ Object
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.
234 235 236 |
# File 'lib/generators/ruact/install/install_generator.rb', line 234 def prime_server_functions_codegen rake "ruact:server_functions:generate" end |
#show_post_install_message ⇒ Object
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 |
# File 'lib/generators/ruact/install/install_generator.rb', line 385 def 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 show_shadcn_next_steps if shadcn? 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 |