import { createComputed, createScope, createSlot, createState, isFunction, isMutableSignal, isSignal, isSlot, type MaybeCleanup, type MemoCallback, type Signal, type State, type TaskCallback, } from '@zeix/cause-effect' import { InvalidComponentNameError } from './errors' import { makeProvideContexts, makeRequestContext, type ProvideContextsHelper, type RequestContextHelper, } from './helpers/context' import { type ElementQueries, makeElementQueries } from './helpers/dom' import { makeOn, type OnHelper } from './helpers/events' import { activateResult, type FactoryResult, type Falsy, makePass, makeWatch, type PassHelper, type WatchHelper, } from './helpers/reactive' import { getSignals } from './internal' import { type ComponentProps, isMethodProducer, isParser, type MethodProducer, type Parser, } from './types' /* === Types === */ /** * Any value that `#setAccessor` can turn into a signal: * - `T` — wrapped in `createState()` * - `Signal` — used directly * - `MemoCallback` — wrapped in `createComputed()` * - `TaskCallback` — wrapped in `createTask()` */ type MaybeSignal = | T | Signal | MemoCallback | TaskCallback /** * The `props` argument of `defineComponent` — a map from property names to their initializers. * * Each value may be: * - A **static value** or **`Signal`** — used directly as the initial signal value. * - A **`Parser`** (branded with `asParser()`) — called with the attribute value string * at connect time. * - A **`MethodProducer`** (branded with `defineMethod()`) — assigned directly as the property * value; the function IS the method. Per-instance state lives in factory scope. */ type Initializers

= { [K in keyof P]?: P[K] | Signal | Parser | MethodProducer } /** * The context object passed to the v1.1 factory function. * * Components destructure only what they need. */ type FactoryContext

= ElementQueries & { host: HTMLElement & P expose: (props: Initializers

) => void watch: WatchHelper

on: OnHelper

pass: PassHelper

provideContexts: ProvideContextsHelper

requestContext: RequestContextHelper } /* === Exported Functions === */ /** * Define and register a reactive custom element using the v1.1 factory form. * * The factory receives a `FactoryContext` at connect time: query helpers (`first`, `all`), * the `host` element, and `expose()` for declaring reactive properties. It returns a flat * array of effect descriptors created by helpers like `watch()`, `on()`, `pass()`, * `provideContexts()`, and `requestContext()`. * * Effects activate after dependency resolution — child custom elements are guaranteed to * be defined before any descriptor runs. * * @since 2.0 * @param {string} name - Custom element name (must contain a hyphen and start with a lowercase letter) * @param {function} factory - Factory function that queries elements, calls expose(), and returns effect descriptors * @throws {InvalidComponentNameError} If the component name is not a valid custom element name */ function defineComponent

( name: string, factory: (context: FactoryContext

) => FactoryResult | Falsy | void, ): CustomElementConstructor | undefined { if (!name.includes('-') || !name.match(/^[a-z][a-z0-9-]*$/)) throw new InvalidComponentNameError(name) class Truc extends HTMLElement { debug?: boolean #initialized = false #setup: FactoryResult = [] #cleanup: MaybeCleanup /** * Native callback when the custom element is first connected to the document */ connectedCallback() { const runSetup = () => { this.#cleanup = createScope( () => { activateResult(this.#setup) }, { root: true, }, ) } if (this.#initialized) { runSetup() } else { const host = this as unknown as HTMLElement & P const [elementQueries, resolveDependencies] = makeElementQueries(host) const context: FactoryContext

= { expose: this.#initSignals.bind(this), host, ...elementQueries, watch: makeWatch(host), on: makeOn(host), pass: makePass(host), provideContexts: makeProvideContexts(host), requestContext: makeRequestContext(host), } const result = factory(context) if (result) this.#setup = result this.#initialized = true if (!this.#setup.length) return resolveDependencies(runSetup) } } /** * Native callback when the custom element is disconnected from the document */ disconnectedCallback() { if (isFunction(this.#cleanup)) this.#cleanup() } /** * Initialize signals for each property in the given initializers map. * Dispatch order: Parser → MethodProducer → static/Signal * * @param {Initializers

} instanceProps - Property initializers to process */ #initSignals(instanceProps: Initializers

): void { const createReactiveProperty = ( key: K, initializer: Initializers

[K], ) => { if (isParser(initializer)) { const result = initializer(this.getAttribute(key)) if (result != null) this.#setAccessor(key, result) } else if (isMethodProducer(initializer)) { ;(this as any)[key] = initializer } else { const value = initializer as MaybeSignal if (value != null) this.#setAccessor(key, value) } } for (const [prop, initializer] of Object.entries(instanceProps)) { if (initializer == null || prop in this) continue createReactiveProperty(prop as keyof P & string, initializer) } } /** * Create or replace the Slot-backed property accessor for a reactive property. * Mutable signals are wrapped in a Slot so their backing signal can be swapped * later (e.g. by `pass()`). * * @since 0.15.0 * @param {K} key - Reactive property name * @param {MaybeSignal} value - Static value, signal, or computed callback */ #setAccessor(key: K, value: MaybeSignal): void { const signal = isSignal(value) ? value : isFunction(value) ? createComputed(value) : (createState(value) as State) const signals = getSignals(this) const k = key as string const prev = signals[k] if (isSlot(prev)) { prev.replace(signal) } else if (isMutableSignal(signal)) { const slot = createSlot(signal) signals[k] = slot Object.defineProperty(this, key, slot) } else { signals[k] = signal Object.defineProperty(this, key, { get: signal.get, enumerable: true, }) } } } customElements.define(name, Truc) return customElements.get(name) } export { defineComponent, type FactoryContext, type Initializers, type MaybeSignal, }