Class: Ruact::ClientManifest

Inherits:
Object
  • Object
show all
Defined in:
lib/ruact/client_manifest.rb

Overview

Reads the react-client-manifest.json emitted by the Vite plugin and resolves component names to Flight ClientReferences.

Manifest format (one entry per “use client” export):

{
  "LikeButton": {
    "id":     "/assets/LikeButton-abc123.js",
    "chunks": ["/assets/LikeButton-abc123.js"],
    "name":   "LikeButton"
  },
  "posts/_like_button": {
    "id":     "/assets/posts/_like_button-abc123.js",
    "chunks": ["/assets/posts/_like_button-abc123.js"],
    "name":   "default"
  }
}

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.from_hash(data) ⇒ Object

Build from an already-parsed Hash (useful in tests). The @reference_cache ivar is initialized eagerly so the freeze + first-lookup path works even when data is empty (otherwise reference_for would raise FrozenError trying to memoize on a frozen instance).



83
84
85
86
87
88
# File 'lib/ruact/client_manifest.rb', line 83

def self.from_hash(data)
  manifest = new
  manifest.instance_variable_set(:@data, data)
  manifest.instance_variable_set(:@reference_cache, {})
  manifest
end

.load(path) ⇒ Object

Load from a file path (JSON). Pre-warms the reference cache and freezes the manifest so it cannot be mutated at runtime (AC#5). Pre-warming is required because Ruby’s freeze is shallow: instance variable assignment on a frozen object raises FrozenError, so @reference_cache must already be set before freeze.



70
71
72
73
74
75
76
# File 'lib/ruact/client_manifest.rb', line 70

def self.load(path)
  raw      = File.read(path)
  data     = JSON.parse(raw)
  manifest = from_hash(data)
  data.each_key { |name| manifest.reference_for(name) }
  manifest.freeze
end

Instance Method Details

#include?(name) ⇒ Boolean

Returns true if name is a top-level key in the manifest data. Used by the dual-path resolver to check co-located key existence before fallback.

Returns:

  • (Boolean)


34
35
36
# File 'lib/ruact/client_manifest.rb', line 34

def include?(name)
  entries_by_name.key?(name)
end

#reference_for(name, controller_path: nil) ⇒ Object

Resolve a component name (e.g. “LikeButton”) → ClientReference.

When controller_path is provided (e.g. “posts”), the resolver first looks for a co-located key (“posts/_like_button”). If found, it returns that reference; otherwise it falls back to the shared PascalCase key.

Returns the same object for repeated calls with the same resolved key (needed for dedup by object_id in Flight::Serializer).

Raises Ruact::ManifestError when the resolved name is not found. The error message includes a Damerau-Levenshtein closest-match suggestion (Story 7.4) when a manifest entry within distance 2 exists, or a file-path hint suggesting where to add the missing component otherwise. When controller_path is given the closest-match scan biases toward co-located keys so a typo inside posts/show.html.erb surfaces the posts/_like_button suggestion before the shared LikeButton entry.



54
55
56
57
58
59
60
61
62
63
# File 'lib/ruact/client_manifest.rb', line 54

def reference_for(name, controller_path: nil)
  @reference_cache ||= {}
  key = resolve_key(name, controller_path)
  @reference_cache[key] ||= begin
    entry = entries_by_name[key]
    raise ManifestError, build_unknown_component_message(name, controller_path) unless entry

    Flight::ClientReference.new(module_id: entry["id"], export_name: entry["name"])
  end
end

#resolve(module_id, _export_name) ⇒ Object

Used by Flight::Serializer to produce I rows. Returns the metadata array the client expects: [id, name, chunks]



25
26
27
28
29
30
# File 'lib/ruact/client_manifest.rb', line 25

def resolve(module_id, _export_name)
  entry = by_module_id(module_id)
  raise "ClientManifest: no entry for module_id=#{module_id.inspect}" unless entry

  [entry["id"], entry["name"], entry["chunks"]]
end