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.



305
306
307
308
309
310
311
312
313
314
# File 'lib/ruact/railtie.rb', line 305

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.



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

def self.check_vite!
  require "socket"
  TCPSocket.new("localhost", 5173).close
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
  Rails.logger.warn "[ruact] Vite dev server not detected at localhost:5173 " \
                    "— run npm run dev for HMR"
end

.controller_paths_for(engine) ⇒ Array<String>

Returns existing controller directory paths.

Parameters:

  • engine (Rails::Engine)

    either ‘Rails.application` or a mounted engine

Returns:

  • (Array<String>)

    existing controller directory paths



238
239
240
241
242
243
244
245
# File 'lib/ruact/railtie.rb', line 238

def self.controller_paths_for(engine)
  return [] unless engine.respond_to?(:config) && engine.config.respond_to?(:paths)

  paths = engine.config.paths["app/controllers"]
  return [] unless paths.respond_to?(:existent)

  paths.existent
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).



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/ruact/railtie.rb', line 140

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_load_dir(dir, glob: "**/*_controller.rb") ⇒ Integer

Returns number of files loaded.

Parameters:

  • dir (String)
  • glob (String) (defaults to: "**/*_controller.rb")

    glob pattern relative to dir; defaults to ‘*/_controller.rb` for back-compat with pre-Story-8.3 callers.

Returns:

  • (Integer)

    number of files loaded



251
252
253
254
255
# File 'lib/ruact/railtie.rb', line 251

def self.force_load_dir(dir, glob: "**/*_controller.rb")
  Dir.glob("#{dir}/#{glob}").each do |file|
    require_dependency(file)
  end.length
end

.force_load_server_function_hosts!Integer Also known as: force_load_controllers!

Story 8.1 review-batch 3 (2026-05-14) — force-loads every controller file under ‘Rails.application.config.paths` so the `ruact_action` registrations populate the registry on a clean boot.

Without this, Rails’ dev-mode lazy autoload only loads a controller when it’s first referenced (typically the first request that routes to it). That means the codegen snapshot in ‘to_prepare` would miss any controller not yet touched.

Implementation: glob the ‘app/controllers` directories listed in the Rails paths configuration and `require_dependency` each `*_controller.rb` file. `require_dependency` works in both Zeitwerk (Rails 7+) and the classic autoloader. On Zeitwerk it is implemented as `Rails.autoloaders.main.load_file(path)` under the hood.

Errors are surfaced as ‘Ruact::Error` with a controller hint so the developer sees a meaningful boot failure instead of a silent skip.

Re-run-6 (2026-05-15) — also walks every mounted ‘Rails::Engine` (and `Rails::Railtie` with `paths`) so engine- owned controllers that declare `ruact_action` populate the registry at boot. Without this an engine’s ‘ruact_action` declarations would only register on first request that touches the engine controller —codegen + endpoint dispatch would lag behind boot.

Returns:

  • (Integer)

    number of controller files loaded.



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/ruact/railtie.rb', line 195

def self.force_load_server_function_hosts!
  loaded = 0

  controller_paths_for(Rails.application).each do |dir|
    loaded += force_load_dir(dir, glob: "**/*_controller.rb")
  end
  server_actions_paths_for(Rails.application).each do |dir|
    loaded += force_load_dir(dir, glob: "**/*.rb")
  end

  if defined?(Rails::Engine)
    Rails::Engine.subclasses.each do |engine_class|
      # Skip the host app itself; `Rails.application.class` is a
      # `Rails::Engine` subclass and was already covered above.
      next if engine_class == Rails.application.class

      engine = safe_engine_instance(engine_class)
      next unless engine

      controller_paths_for(engine).each { |dir| loaded += force_load_dir(dir, glob: "**/*_controller.rb") }
      server_actions_paths_for(engine).each { |dir| loaded += force_load_dir(dir, glob: "**/*.rb") }
    end
  end

  loaded
rescue LoadError, NameError => e
  raise Ruact::Error,
        "ruact: failed to force-load a server-function host while populating " \
        "Ruact.action_registry: #{e.class}: #{e.message}. The gem " \
        "force-loads `app/controllers/**/*_controller.rb` and " \
        "`app/server_actions/**/*.rb` at `config.to_prepare` so " \
        "registries are complete on first boot."
end

.safe_engine_instance(engine_class) ⇒ Object

Some engine subclasses are abstract (no ‘.instance` defined yet) or fail at `.instance` if their config block raises. Swallow those quietly —the gem’s responsibility is to load whatever it can; engines whose ‘.instance` blows up will surface on first request anyway.



282
283
284
285
286
# File 'lib/ruact/railtie.rb', line 282

def self.safe_engine_instance(engine_class)
  engine_class.instance
rescue StandardError
  nil
end

.server_actions_paths_for(engine) ⇒ Object

Story 8.3 — locates ‘app/server_actions/` for the host application or a mounted engine. Uses the Rails `paths` enumerator (populated by the Railtie initializer) when present, falling back to a direct `engine.root.join` lookup for engines that haven’t registered the path. Returns an empty array when the directory doesn’t exist —silent no-op for hosts that don’t use standalone actions.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/ruact/railtie.rb', line 263

def self.server_actions_paths_for(engine)
  if engine.respond_to?(:config) && engine.config.respond_to?(:paths)
    paths = engine.config.paths["app/server_actions"]
    if paths.respond_to?(:existent)
      existent = paths.existent
      return existent unless existent.empty?
    end
  end

  return [] unless engine.respond_to?(:root) && engine.root

  candidate = engine.root.join("app/server_actions")
  candidate.directory? ? [candidate.to_s] : []
end

.write_server_functions_snapshot!Boolean

Writes the server-functions JSON snapshot to tmp/cache/ruact/ on every config.to_prepare. The write is short-circuited when the registry payload is unchanged (Story 8.0a — pitfall #1: dev mode fires to_prepare per request; a naive rewrite would burn IOPS and confuse the Vite plugin’s chokidar watcher).

Returns:

  • (Boolean)

    true if a fresh file was written, false if unchanged.



295
296
297
298
299
300
301
# File 'lib/ruact/railtie.rb', line 295

def self.write_server_functions_snapshot!
  Ruact::ServerFunctions::Snapshot.generate!(
    action_registry: Ruact.action_registry,
    query_registry: Ruact.query_registry,
    path: Rails.root.join("tmp/cache/ruact/server-functions.json")
  )
end