Class: Ruact::ClientManifest
- Inherits:
-
Object
- Object
- Ruact::ClientManifest
- 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"
}
}
Story 13.5 (FR100) — an entry MAY carry an optional, purely additive
contract field when the component opts in by exporting __ruactContract
from its .tsx (the Vite plugin extracts it names-only). Shape:
"LikeButton": {
"id": ..., "name": ..., "chunks": [...],
"contract": {
"props": { "postId" => "required", "initialCount" => "optional" },
"slots": { "header" => "optional" }, # optional
"passthrough": false # optional
}
}
A component without the export has NO contract key (back-compatible: every
existing manifest + reader is unaffected). #contract_for reads it for the
Story 13.5 preprocess-time call-site validator; nil means "no contract →
no validation" (fail open).
Class Method Summary collapse
-
.from_hash(data) ⇒ Object
Build from an already-parsed Hash (useful in tests).
-
.load(path) ⇒ Object
Load from a file path (JSON).
Instance Method Summary collapse
-
#contract_for(name, controller_path: nil) ⇒ Hash?
Story 13.5 (FR100) — return the optional component
contractHash forname, ornilwhen the component declared none (or is absent from the manifest). -
#include?(name) ⇒ Boolean
Returns true if
nameis a top-level key in the manifest data. -
#reference_for(name, controller_path: nil) ⇒ Object
Resolve a component name (e.g. "LikeButton") → ClientReference.
-
#resolve(module_id, _export_name) ⇒ Object
Used by Flight::Serializer to produce I rows.
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).
119 120 121 122 123 124 |
# File 'lib/ruact/client_manifest.rb', line 119 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.
106 107 108 109 110 111 112 |
# File 'lib/ruact/client_manifest.rb', line 106 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
#contract_for(name, controller_path: nil) ⇒ Hash?
Story 13.5 (FR100) — return the optional component contract Hash for
name, or nil when the component declared none (or is absent from the
manifest). Honors the same co-located/shared resolve_key precedence as
#reference_for, so a co-located component's contract is found when the
call site's controller_path is known. A pure read (no memoization, no
mutation) — safe on the frozen manifest, never raises for an unknown name
(the validator fails open). Consumed by Ruact::ComponentContract from
the ERB preprocessor.
96 97 98 99 |
# File 'lib/ruact/client_manifest.rb', line 96 def contract_for(name, controller_path: nil) entry = entries_by_name[resolve_key(name, controller_path)] entry && entry["contract"] end |
#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.
52 53 54 |
# File 'lib/ruact/client_manifest.rb', line 52 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.
72 73 74 75 76 77 78 79 80 81 |
# File 'lib/ruact/client_manifest.rb', line 72 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, (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]
43 44 45 46 47 48 |
# File 'lib/ruact/client_manifest.rb', line 43 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 |