Class: Ruact::Railtie

Inherits:
Rails::Railtie
  • Object
show all
Defined in:
lib/ruact/railtie.rb

Class Method Summary collapse

Class Method Details

.check_manifest!(manifest_path) ⇒ Object

Checks whether the manifest exists and either warns (dev) or raises (prod). Extracted as a class method for direct testability without a full Rails app.



172
173
174
175
176
177
178
179
180
181
# File 'lib/ruact/railtie.rb', line 172

def self.check_manifest!(manifest_path)
  if Rails.env.production?
    raise ManifestError,
          "react-client-manifest.json not found — run vite build before deploying"
  else
    Rails.logger.warn "[ruact] react-client-manifest.json not found at " \
                      "#{manifest_path} — RSC rendering will be unavailable. " \
                      "Run 'npm run build' or start the Vite dev server."
  end
end

.check_vite!Object

Checks whether the Vite dev server is accessible and warns if not (AC#4). Extracted as a class method for direct testability without a full Rails app.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ruact/railtie.rb', line 101

def self.check_vite!
  require "socket"
  require "uri"
  uri  = URI.parse(Ruact.config.vite_dev_server)
  host = uri.host || "localhost"
  port = uri.port || 5173
  # `connect_timeout` so a blackholed/remote configured host can't stall dev
  # boot from `after_initialize` until the OS TCP timeout (matches
  # ViewHelper#vite_dev_running?).
  TCPSocket.new(host, port, connect_timeout: 1).close
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, SocketError
  # Broad rescue (incl. SocketError) because the host is now configurable: a
  # misconfigured/unresolvable `vite_dev_server` must downgrade to this dev
  # warning, never crash boot from `after_initialize`. `host`/`port` are
  # assigned before the connect attempt, so they are always in scope here.
  Rails.logger.warn "[ruact] Vite dev server not detected at #{host}:#{port} " \
                    "— run npm run dev for HMR"
end

.detect_streaming_mode!Object

Detects the web server at boot, stores the streaming mode, and logs the result (AC#1–3). Detection is constant-based (zero I/O): Puma → enabled, Unicorn/Passenger → buffered, unknown → buffered (safe mode).



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ruact/railtie.rb', line 80

def self.detect_streaming_mode!
  mode, label = if defined?(::Puma::Server)
                  [:enabled,  "Puma detected"]
                elsif defined?(::Falcon::Server)
                  [:enabled,  "Falcon detected"]
                elsif defined?(::Unicorn)
                  [:buffered, "Unicorn detected"]
                elsif defined?(::PhusionPassenger)
                  [:buffered, "Passenger detected"]
                else
                  [:buffered, "server unknown — defaulting to safe mode"]
                end

  Ruact.streaming_mode = mode
  verb = mode == :enabled ? "enabled" : "buffered"
  Rails.logger.info "[ruact] streaming: #{verb} (#{label})"
  mode
end

.force_routes_loaded!Boolean

Idempotently force the route table to load. Returns true when the table is safely loaded (so the caller may read it), false when the force re-entered the boot finisher's in-progress draw (Rails < 8 raises FrozenError there) — in which case the caller must NOT write, leaving the table to the routes initializer + the later after_initialize write.

Returns:

  • (Boolean)

    true if routes are safe to read, false if mid-draw.



160
161
162
163
164
165
166
167
168
# File 'lib/ruact/railtie.rb', line 160

def self.force_routes_loaded!
  reloader = Rails.application.routes_reloader
  return true unless reloader.respond_to?(:execute_unless_loaded)

  reloader.execute_unless_loaded
  true
rescue FrozenError
  false
end

.write_server_functions_snapshot!Array<Hash>?

Story 9.9 — writes the route-driven (v2) server-functions JSON snapshot to the REAL bridge (tmp/cache/ruact/server-functions.json) on every config.to_prepare. The Vite plugin renders this JSON into app/javascript/.ruact/server-functions.ts. The write is short-circuited when the entries are unchanged (Story 8.0a pitfall #1 — dev mode fires to_prepare per request; a naive rewrite would burn IOPS and churn the Vite plugin's chokidar watcher).

Cold-boot ordering (folded in from the Story 9.8 host workaround): on the very first boot to_prepare can run BEFORE routes.rb is drawn, leaving the route set empty so the codegen would emit zero functions. Force the route table to load first via routes_reloader.execute_unless_loaded so RouteSource/QuerySource see every route.

The force-load is guarded: when to_prepare fires from INSIDE the boot finisher (Rails draws routes during initialize!, and on Rails < 8 re-entering execute_unless_loaded there raises FrozenError against the in-progress route set), we SKIP THE WRITE ENTIRELY rather than publish a snapshot from a mid-draw/incomplete route set (which Vite could observe as a transient empty/stale bridge — Codex R1). The authoritative boot write is the after_initialize call, which always sees the fully-drawn table; the dev-reload to_prepare then refreshes from a settled table.

Returns:

  • (Array<Hash>, nil)

    the exposed v2 entries, or nil if the write was skipped because routes were mid-draw.



145
146
147
148
149
150
151
# File 'lib/ruact/railtie.rb', line 145

def self.write_server_functions_snapshot!
  return nil unless force_routes_loaded!

  Ruact::ServerFunctions.write_v2_snapshot!(
    route_set: Rails.application.routes, root: Rails.root
  )
end