@statorjs/stator 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,27 @@
1
- import type { AnyMachineDef, InstanceOf as SelectorsOf } from '../engine/index.ts'
1
+ import type {
2
+ AnyMachineDef,
3
+ EventOf,
4
+ InstanceOf as SelectorsOf,
5
+ StateNameOf,
6
+ } from '../engine/index.ts'
2
7
  import type { EventDescriptor } from '../server/render-context.ts'
3
8
 
4
9
  /**
5
10
  * The template-facing shape of a machine instance: the engine's selector view
6
11
  * (each selector as a property carrying its return type, callable if it returns
7
- * a function) plus the framework-provided `send` / `state` / `snapshot`.
12
+ * a function) plus the framework-provided `send` / `state` / `snapshot`. Both
13
+ * `state` (the state-name union) and `send` (the event union) are typed to the
14
+ * machine's own def, so `s.state === 'ready'` and `m.send({ type: 'SAVE', … })`
15
+ * autocomplete and a typo — in the name or the payload — is a compile error.
8
16
  */
9
- export type InstanceOf<TDef extends AnyMachineDef> = SelectorsOf<TDef> & InstanceCommon
17
+ export type InstanceOf<TDef extends AnyMachineDef> = SelectorsOf<TDef> &
18
+ InstanceCommon<StateNameOf<TDef>, EventOf<TDef>>
10
19
 
11
- export interface InstanceCommon<TStateKey extends string = string> {
12
- send(event: { type: string; [k: string]: unknown }): EventDescriptor | undefined
20
+ export interface InstanceCommon<
21
+ TStateKey extends string = string,
22
+ TEvent = { type: string; [k: string]: unknown },
23
+ > {
24
+ send(event: TEvent): EventDescriptor | undefined
13
25
  readonly state: TStateKey
14
26
  readonly snapshot: unknown
15
27
  }
package/src/wire/apply.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * CustomEvent on `window` (the inspector's contract), regardless of which
9
9
  * path applied it.
10
10
  */
11
+ import { setAttr } from './attr-value.ts'
11
12
  import type { Directive, Patch } from './index.ts'
12
13
  import {
13
14
  elementAt,
@@ -74,8 +75,7 @@ function applyPatch(
74
75
  return null
75
76
  }
76
77
  if (patch.op === 'text') element.textContent = patch.value
77
- else if (patch.value === null) element.removeAttribute(patch.name)
78
- else element.setAttribute(patch.name, patch.value)
78
+ else setAttr(element, patch.name, patch.value)
79
79
  return element
80
80
  }
81
81
 
@@ -0,0 +1,55 @@
1
+ import { isUrlAttribute, safeAttrUrl } from './safe-url.ts'
2
+
3
+ /**
4
+ * THE attribute-value contract — one implementation for every tier that
5
+ * writes an attribute: static render (template/html.ts), the live diff
6
+ * (server/recompute.ts), the island writer codegen (compiler/client-emit.ts,
7
+ * via the client re-export), and the wire applier (wire/apply.ts).
8
+ *
9
+ * Boolean-absent semantics: `false`/`null`/`undefined` → `null`, meaning the
10
+ * attribute is ABSENT (`checked={expr}` must be able to render unchecked, and
11
+ * a static render must agree with what a live patch of the same attribute
12
+ * does). `true` → present-and-empty. Arrays join with no separator (matching
13
+ * text interpolation); everything else stringifies.
14
+ *
15
+ * History: this contract used to live in four drifting copies, and two of
16
+ * them disagreed in ways users hit — `checked={false}` rendered
17
+ * `checked="false"` on the static path, and array values patched as "a,b"
18
+ * but rendered as "ab". One implementation, so the seams can't disagree.
19
+ */
20
+ export function attrValue(v: unknown): string | null {
21
+ if (v === false || v === null || v === undefined) return null
22
+ if (v === true) return ''
23
+ return stringifyAttr(v)
24
+ }
25
+
26
+ function stringifyAttr(v: unknown): string {
27
+ if (v == null) return ''
28
+ if (typeof v === 'string') return v
29
+ if (Array.isArray(v)) return v.map(stringifyAttr).join('')
30
+ return String(v)
31
+ }
32
+
33
+ /** The text-position value contract, shared by static render and text
34
+ * patches: null/undefined → empty, arrays join with no separator (an
35
+ * interpolated list of fragments/strings renders concatenated, not
36
+ * comma-joined), everything else stringifies. */
37
+ export function textValue(v: unknown): string {
38
+ return stringifyAttr(v)
39
+ }
40
+
41
+ /** Scheme guard for url-bearing attributes (href/src/…): strip a
42
+ * javascript:/vbscript: value whether it arrives at first render or through
43
+ * a later diff. `null` (attribute-absent) passes through. */
44
+ export function sanitizeAttr(name: string, value: string): string
45
+ export function sanitizeAttr(name: string, value: string | null): string | null
46
+ export function sanitizeAttr(name: string, value: string | null): string | null {
47
+ if (value === null) return null
48
+ return isUrlAttribute(name) ? safeAttrUrl(value) : value
49
+ }
50
+
51
+ /** The DOM half of the contract: `null` removes, a string sets. */
52
+ export function setAttr(el: Element, name: string, value: string | null): void {
53
+ if (value === null) el.removeAttribute(name)
54
+ else el.setAttribute(name, value)
55
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Island marker formats — the compiler↔island-runtime contract, in one place
3
+ * so codegen (compiler/lower.ts, compiler/client-emit.ts) and the runtime
4
+ * (client/bind.ts's bindSlot, the generated querySelector calls) can't drift.
5
+ *
6
+ * - ELEMENT markers: `data-b="b<N>"` on an element whose on:/bind:/read()
7
+ * attribute wiring the island's setup() attaches. Every occurrence of a
8
+ * marker is wired (a marked element inside a `.map()` repeats per row).
9
+ * - TEXT SLOT markers: an `<!--s<N>-->` comment where a client-machine
10
+ * `read()` sat in text position; the runtime materializes one text node
11
+ * after each occurrence. The comment's data IS the marker string.
12
+ */
13
+
14
+ export const ISLAND_MARKER_ATTR = 'data-b'
15
+
16
+ export const elementMarker = (i: number): string => `b${i}`
17
+
18
+ export const slotMarker = (i: number): string => `s${i}`
19
+
20
+ /** CSS selector for every occurrence of an element marker. */
21
+ export const elementMarkerSelector = (marker: string): string =>
22
+ `[${ISLAND_MARKER_ATTR}="${marker}"]`