Noizu.Tessera.Component.Extension (Noizu.Tessera v0.2.0)

Copy Markdown View Source

The shared Elixir derivation layer behind use Vendor.Element, tag: … (EXTENSION-CONTRACT.md §5). Runtime-agnostic on purpose: Tier 1 (compiled to Lit) uses it now, and Tier 2 (Hologram-in-element, brief 3f) reuses the same merge + dispatcher generation — the spec is the single interface between definition and runtime (§9).

Responsibilities:

  • derive!/3 — the expansion of the base module's generated __using__/1 macro: option validation (tag: / :inherit / priority: / extension_api:), the double-use and cycle guards, and the AST that re-emits the inherited Hologram props and internally uses Noizu.Tessera.Component in derivation mode;
  • merge_spec!/3 — deep-copy the base spec's declarations and layer the derived module's deltas: additive new names append in declaration order, override-by-name replaces wholesale, removal is forbidden, required: may relax not tighten, and the collision/duplicate/round-trip/reserved checks re-run over the MERGED spec (the caller runs validate_all!);
  • check_pin!/3 — the §8 extension-api semver pin (major mismatch raises, minor-range mismatch warns);
  • chain construction (§5.9, brief 2e) — takeover?/1, chain_depth/1, chain_order/1 (dependency depth → priority: → module name, innermost first — the mirror of the JS registry comparator), record_takeover_layer!/2 (the app-wide sibling registry + the §5.7 chain-conflict warning), and artifact_stem/1 so sibling takeover layers never clobber each other's base-tag-keyed artifacts (§5.8);
  • dispatcher generation — base_init/2,3 (init hooks) and the base_action/3 scaffolding Tier 2 consumes. Hologram's IR has no super, so the Elixir down-call is an injected dispatcher, never language-level super. Tier-1 fn-actions never compile to the BEAM; their down-call is real ES super in the lowered JS (see Noizu.Tessera.Component.Expr), so no BEAM base_action/3 is generated for them.

Inherited entries in a merged spec carry inherited: true; the derived module's own (new or overriding) entries do not. The Lit lowering uses that marker to emit only new/overridden members and inherit the rest through the ES class chain.

Summary

Functions

The canonical Tier-2 element macro, invoked by a vendor module as use Noizu.Tessera.Component.Extension, … (D4: the vendor module IS the macro — the __using__ its extenders consume is generated here, not hand-rolled). Options (compile-time literals)

Tier-2 base_action/3 dispatcher scaffolding (brief 3f): one clause per inherited action, dispatching to the base module's __tessera_action__/3 entry point. Generated ONLY when the base actually exports that entry point (a Hologram-runtime component). Tier-1 fn-actions never compile to the BEAM and have no __tessera_action__/3, so this returns [] for them — their down-call is real ES super in the lowered JS, not a BEAM dispatcher.

The artifact filename STEM for a spec. Fresh and fresh-tag-derived specs keep the plain tag (byte-stable goldens). A takeover layer registers under the BASE tag (§5.8), and sibling layers would otherwise clobber each other's base-tag-keyed element/contract files — so takeover artifacts carry the declaring module: <base-tag>__<module_snake>.

Layers above the vendor base along the derived_from spine (§5.9 dependency order): 0 for a fresh component, 1 for the first :inherit layer, and so on. A cycle cannot reach here — guard_cycle!/2 rejects it at expansion.

Sorts takeover-chain layer descriptors INNERMOST → OUTERMOST per §5.9: dependency depth first, then priority: (higher sits further out), then module name ascending (alphabetically-first sits closer to the base). The outermost layer runs first and wins conflicts. Entries are maps with :depth, :priority and :module (string). This is the compile-time mirror of priv/js/src/extend.mjs's comparator (which adds a first-registered-outermost fallback for unlabelled hand-written mixins).

Checks a derived extension_api: pin against the base's declared version. Unsatisfied MAJOR raises CompileError; an unsatisfied minor range warns (host policy decides whether to fail). nil requirement is a no-op. A base that declares no version is treated as the first stable 1.0.0.

Builds the AST spliced into a deriving module by the base module's generated __using__/1. base_spec is the base module's compiled spec; opts are the raw use Base, … options (AST); caller is the deriving module's env.

The expansion of a Tier-2 vendor's generated __using__/1 (use Vendor, tag: … — §5.3, D4). Runs the SAME shared layer Tier 1 uses — option guards, the §5.9 chain machinery, merge_spec!/3 (hence the §5.5 merge rules + the T1-4/T2-7 reference-graph type-change guard) and the §8 pin check — then splices the Tier-2 lowering: a Hologram component whose inherited surface is reachable through the injected base_action/3 dispatcher + defoverridable (Hologram's IR has no super; D4 forbids promising Elixir-level super).

Injected init-hook dispatchers for a deriving module: base_init/2 and base_init/3 reach the base module's (possibly overridden) __tessera_init__/2,3. The derived module's own __tessera_init__ defaults dispatch through these, so an unoverridden hook still runs the base's init (§5.3). Names mirror base_action/3 (T1-2 resolved).

Deep-copies the base spec's declarations and layers the derived module's deltas (a map of raw author entries keyed by kind). Returns a merged declaration map the caller feeds through validate_all! + the element translator. Inherited entries carry inherited: true.

Records a compiled tag: :inherit layer in the app-wide sibling registry and emits the §5.7 CHAIN-CONFLICT warning for every same-depth sibling that overrides an action body or a template region this layer also overrides. Called from Component.__before_compile__/1 once the merged spec exists: whichever sibling compiles SECOND sees the first and warns (compile order never hides the conflict). The chain itself needs no re-ordering here — depth/priority ride each spec + the JS registry entries, and the runtime composition IS chain_order/1 at define time.

snake form of artifact_stem/1 (contract filenames).

True when spec is a tag: :inherit tag-takeover: it derives from another module AND registers under the BASE tag. Fresh tags MUST differ from the base tag (resolve_tag!/3), so derived_from + tag equality is exactly the takeover condition — no extra spec field, and goldens/manifests for fresh-tag specs are untouched.

Functions

__using__(opts)

(macro)

The canonical Tier-2 element macro, invoked by a vendor module as use Noizu.Tessera.Component.Extension, … (D4: the vendor module IS the macro — the __using__ its extenders consume is generated here, not hand-rolled). Options (compile-time literals):

  • tag: (required) — the vendor's element tag;
  • actions: — snake atoms in declaration order, the §6.1 OPEN surface (the runtime bodies are the vendor's own def action/3 clauses — Hologram-transpilable Elixir, never fn-AST DSL bodies);
  • props: — name: [type: :, default: :] triples mirroring the vendor's Hologram prop/2,3 declarations (re-emitted into extenders);
  • regions: — atoms naming the arity-1 template helper functions the markup calls (e.g. [:row_label]) — extenders get delegating def name(arg), do: Vendor.name(arg) + defoverridable;
  • template: — the markup string (the same markup the vendor's own def template renders) — rides spec.template.raw so extenders re-emit it and the T1-4/T2-7 reference-graph guard can walk it;
  • extension_api: — the §8 semver declaration (defaults to 1.0.0).

The expansion splices the CONTRACT scaffold onto the vendor: __tessera_spec__/0 + __tessera_manifest__/0 (the minimal Tier-2 spec — the interface extenders merge against), the per-action literal __tessera_action__/3 entry clauses (static calls into the vendor's own action/3; unknown names raise the boxed FunctionClauseError the §6.3 unknown-action classification expects), and the generated __using__/1 that drives derive_tier2!/3 for every extender.

action_dispatcher_asts(base_module, action_names)

@spec action_dispatcher_asts(module(), [atom()]) :: [Macro.t()]

Tier-2 base_action/3 dispatcher scaffolding (brief 3f): one clause per inherited action, dispatching to the base module's __tessera_action__/3 entry point. Generated ONLY when the base actually exports that entry point (a Hologram-runtime component). Tier-1 fn-actions never compile to the BEAM and have no __tessera_action__/3, so this returns [] for them — their down-call is real ES super in the lowered JS, not a BEAM dispatcher.

artifact_stem(spec)

@spec artifact_stem(Noizu.Tessera.Spec.t()) :: String.t()

The artifact filename STEM for a spec. Fresh and fresh-tag-derived specs keep the plain tag (byte-stable goldens). A takeover layer registers under the BASE tag (§5.8), and sibling layers would otherwise clobber each other's base-tag-keyed element/contract files — so takeover artifacts carry the declaring module: <base-tag>__<module_snake>.

chain_depth(spec)

@spec chain_depth(Noizu.Tessera.Spec.t()) :: non_neg_integer()

Layers above the vendor base along the derived_from spine (§5.9 dependency order): 0 for a fresh component, 1 for the first :inherit layer, and so on. A cycle cannot reach here — guard_cycle!/2 rejects it at expansion.

chain_order(entries)

@spec chain_order([map()]) :: [map()]

Sorts takeover-chain layer descriptors INNERMOST → OUTERMOST per §5.9: dependency depth first, then priority: (higher sits further out), then module name ascending (alphabetically-first sits closer to the base). The outermost layer runs first and wins conflicts. Entries are maps with :depth, :priority and :module (string). This is the compile-time mirror of priv/js/src/extend.mjs's comparator (which adds a first-registered-outermost fallback for unlabelled hand-written mixins).

check_pin!(base_version, requirement, env)

@spec check_pin!(String.t() | nil, String.t() | nil, Macro.Env.t()) :: :ok

Checks a derived extension_api: pin against the base's declared version. Unsatisfied MAJOR raises CompileError; an unsatisfied minor range warns (host policy decides whether to fail). nil requirement is a no-op. A base that declares no version is treated as the first stable 1.0.0.

derive!(base_spec, opts, caller)

@spec derive!(Noizu.Tessera.Spec.t(), keyword(), Macro.Env.t()) :: Macro.t()

Builds the AST spliced into a deriving module by the base module's generated __using__/1. base_spec is the base module's compiled spec; opts are the raw use Base, … options (AST); caller is the deriving module's env.

Validates at expansion time (pointed CompileErrors, §5.7) and returns code that flags the module as derived, internally uses Noizu.Tessera.Component, and re-emits the inherited Hologram props so the adapter's @__props__ accumulator is complete before Hologram's __before_compile__ runs.

derive_tier2!(vendor_module, opts, caller)

@spec derive_tier2!(module(), keyword(), Macro.Env.t()) :: Macro.t()

The expansion of a Tier-2 vendor's generated __using__/1 (use Vendor, tag: … — §5.3, D4). Runs the SAME shared layer Tier 1 uses — option guards, the §5.9 chain machinery, merge_spec!/3 (hence the §5.5 merge rules + the T1-4/T2-7 reference-graph type-change guard) and the §8 pin check — then splices the Tier-2 lowering: a Hologram component whose inherited surface is reachable through the injected base_action/3 dispatcher + defoverridable (Hologram's IR has no super; D4 forbids promising Elixir-level super).

Multi-level chaining (T2-9 gap fix): the scaffold records the PARENT (the module the extender used) and links through it — per-action base_action/3 clauses dispatch to the parent's __tessera_action__/3 entry, and the dynamic-name fallback dispatches the parent's action/3 (whose clauses/catch-all continue down-chain), so A→B→C never re-includes the whole stack.

init_dispatcher_asts(base_module)

@spec init_dispatcher_asts(module()) :: [Macro.t()]

Injected init-hook dispatchers for a deriving module: base_init/2 and base_init/3 reach the base module's (possibly overridden) __tessera_init__/2,3. The derived module's own __tessera_init__ defaults dispatch through these, so an unoverridden hook still runs the base's init (§5.3). Names mirror base_action/3 (T1-2 resolved).

merge_spec!(base_spec, deltas, env)

@spec merge_spec!(Noizu.Tessera.Spec.t(), map(), Macro.Env.t()) :: map()

Deep-copies the base spec's declarations and layers the derived module's deltas (a map of raw author entries keyed by kind). Returns a merged declaration map the caller feeds through validate_all! + the element translator. Inherited entries carry inherited: true.

Rules (§5.5): additive new names append in declaration order; override-by-name replaces wholesale (events merge detail, keeping inherited keys); removal is forbidden; required: may relax not tighten; retyping an inherited attr/prop/state key the base READS is rejected (the type-change reference-graph guard). Duplicate AUTHOR declarations raise here (the merge would otherwise silently last-wins them).

Event renames (§5.6, brief 2d): event "new", renames: "inherited" replaces the inherited name in place (detail inherited keys kept, renamed_from marker). The rename is then propagated over the merged emission points — inherited action/lifecycle JS that still emits the OLD event name is rewritten to the new one and marked re_emit: true so the derived class re-emits the method and shadows the base's (which would otherwise emit the dead name).

record_takeover_layer!(spec, env)

@spec record_takeover_layer!(Noizu.Tessera.Spec.t(), Macro.Env.t()) :: :ok

Records a compiled tag: :inherit layer in the app-wide sibling registry and emits the §5.7 CHAIN-CONFLICT warning for every same-depth sibling that overrides an action body or a template region this layer also overrides. Called from Component.__before_compile__/1 once the merged spec exists: whichever sibling compiles SECOND sees the first and warns (compile order never hides the conflict). The chain itself needs no re-ordering here — depth/priority ride each spec + the JS registry entries, and the runtime composition IS chain_order/1 at define time.

The registry lives in :persistent_term keyed by the BASE TAG: it must outlive any single module's compile (siblings expand in separate rounds) and one re-compiling module never warns against itself.

snake_stem(spec)

@spec snake_stem(Noizu.Tessera.Spec.t()) :: String.t()

snake form of artifact_stem/1 (contract filenames).

takeover?(spec)

@spec takeover?(Noizu.Tessera.Spec.t()) :: boolean()

True when spec is a tag: :inherit tag-takeover: it derives from another module AND registers under the BASE tag. Fresh tags MUST differ from the base tag (resolve_tag!/3), so derived_from + tag equality is exactly the takeover condition — no extra spec field, and goldens/manifests for fresh-tag specs are untouched.