Consuming noizu_tessera in a Hologram app

Copy Markdown View Source

Setup for host applications. Component authoring is docs/AUTHORING.md; the normative wire contract is docs/CONTRACT.md.

1. Dependency

# mix.exs
defp deps do
  [
    {:hologram, "~> 0.10"},
    {:noizu_tessera, "~> 0.1"}
  ]
end

noizu_tessera keeps hologram as an optional runtime:false dependency, so non-Hologram hosts (plain Elixir + npm consumers of the generated contracts) depend on it without pulling the framework.

2. Install the JS side

Add the dependency yourself first (§1 above — mix tessera.install does NOT edit mix.exs), then run:

mix tessera.install
  • wires @noizu/tessera-core into assets/package.json (file: link into the installed dependency's priv/js, always a relocatable RELATIVE path — including when the dep lives outside the app tree — plus lit ^3 as a peer; existing keys are never overwritten; re-runs are idempotent; dependencies that ship their own tessera priv/js runtime get file: entries too);
  • runs npm install unless you pass --no-npm; npm missing from PATH fails with install guidance instead of a raw error.

Hosts that manage their own JS packaging can skip the npm leg and add @noizu/tessera-core from their registry of choice themselves.

Boot hazard: export HOLOGRAM_START=1 for the whole boot chain

In a fresh Linux environment the canonical boot chain mix deps.get && mix tessera.install && mix holo can hang indefinitely: mix deps.get compiles path deps immediately, and if that compile runs without HOLOGRAM_START=1, the later mix holo (which sets it) finds the Hologram compiler already registered and loops forever on Hologram: compiler already running, waiting... — hologram 0.10.1 never times out. Observed twice in an elixir:1.18-otp-27 container, never on macOS.

Gate the WHOLE chain on the flag, before mix deps.get:

export HOLOGRAM_START=1
mix deps.get && mix tessera.install && mix holo

CI's e2e jobs export it for the whole chain as a hardening; the full investigation lives in docs/notes/e2e-linux-ci.md (§ "Boot-recipe hazard").

3. Declare components

Author components per docs/AUTHORING.md. Compile — the contracts, __tessera_spec__, and (in Hologram) the render/bridge glue are generated at compile time; nothing to run by hand.

4. Aggregate manifests

defmodule MyApp.Tessera do
  use Noizu.Tessera.Package, components: [MyApp.Components.PersonaCard, ...]
end
mix tessera.manifest

writes tessera-manifest.json (full specs) and custom-elements.json (Custom Elements Manifest, schemaVersion "1.0.0") into the current directory — commit them where your tooling or downstream consumers expect.

mix tessera.cem writes just the CEM for the current project's package(s) (--package Module to scope, --output PATH to relocate). The package module also exposes it programmatically as cem/0.

mix tessera.gen.gallery scaffolds a framework-agnostic interactive gallery from the package CEM — live demos, typed attr/prop editors, slot-content editing, CSS custom-property theming and an event log per component. See docs/AUTHORING.md § Gallery for the build/serve steps; examples/persona_kit/gallery is a working, e2e-tested instance.

5. Runtime model (what the wiring does)

  • Server render: tessera components render as their tag with kebab-case attributes; boolean attrs render by presence (false/nil drop the attribute); json props ship as tsr-props (camelCase keys, HTML-encoded in the served HTML, decoded by the DOM); bound events render as data-tsr-on="event:action@target".
  • Registration: the top-level register() call at the end of each component's .mjs is load-bearing — stateless SSR never calls init/3, so the element gets defined only when the page bundle evaluates the .mjs and reaches that statement. The .mjs itself is bundled because the generated init/2/init/3 pair calls its register export through an absolute-path js_import binding, keeping it on the page call graph. mix compile warns if a component's .mjs is missing the top-level call. Stateful instances additionally register through those inits. defineOnce in @noizu/tessera-core makes duplicate registration (multiple bundles) warn-once instead of throwing.
  • Events: element → Elixir actions flow through the bridge described in CONTRACT.md §4. Bind an action atom (on_persona_select={:chose}) or a tuple with an explicit target ({:chose, target: "layout"} or {:chose, target: cid} — CONTRACT.md §3 supports cid targets only through the tuple form; bare binaries are rejected with an ArgumentError). With NO binding (nil/false, or none passed) NO data-tsr-on entry is rendered for that event and NOTHING dispatches when the element emits it — the event is then just a plain composed DOM CustomEvent that only plain addEventListener consumers see. There is no implicit default action.

6. Versioning note

The Elixir package and @noizu/tessera-core version in lockstep (0.1.x) — the wire contract (v1) is what they share, so minor mismatches are tolerable, but keep them in the same major.