@hypen-space/core 0.4.980 → 0.5.3

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.
Files changed (100) hide show
  1. package/README.md +5 -8
  2. package/dist/a11y.d.ts +82 -0
  3. package/dist/app.js +13 -1168
  4. package/dist/app.js.map +3 -9
  5. package/dist/chunk-00zt3w6e.js +58 -0
  6. package/dist/chunk-00zt3w6e.js.map +10 -0
  7. package/dist/chunk-3zvyth50.js +87 -0
  8. package/dist/chunk-3zvyth50.js.map +10 -0
  9. package/dist/chunk-5gtx3pza.js +11 -0
  10. package/dist/chunk-5gtx3pza.js.map +9 -0
  11. package/dist/chunk-7222131a.js +191 -0
  12. package/dist/chunk-7222131a.js.map +10 -0
  13. package/dist/chunk-avchpwhf.js +200 -0
  14. package/dist/chunk-avchpwhf.js.map +10 -0
  15. package/dist/chunk-ec0tgy9a.js +154 -0
  16. package/dist/chunk-ec0tgy9a.js.map +10 -0
  17. package/dist/chunk-fnss4dm2.js +479 -0
  18. package/dist/chunk-fnss4dm2.js.map +10 -0
  19. package/dist/chunk-g2gz9pgg.js +133 -0
  20. package/dist/chunk-g2gz9pgg.js.map +10 -0
  21. package/dist/chunk-g72v6mmf.js +341 -0
  22. package/dist/chunk-g72v6mmf.js.map +10 -0
  23. package/dist/chunk-g7g3jxty.js +351 -0
  24. package/dist/chunk-g7g3jxty.js.map +10 -0
  25. package/dist/chunk-g830re2c.js +50 -0
  26. package/dist/chunk-g830re2c.js.map +10 -0
  27. package/dist/chunk-gtwh1sg7.js +127 -0
  28. package/dist/chunk-gtwh1sg7.js.map +10 -0
  29. package/dist/chunk-k12cwfst.js +72 -0
  30. package/dist/chunk-k12cwfst.js.map +10 -0
  31. package/dist/chunk-mbqqsz6r.js +128 -0
  32. package/dist/chunk-mbqqsz6r.js.map +10 -0
  33. package/dist/chunk-ne632swm.js +209 -0
  34. package/dist/chunk-ne632swm.js.map +10 -0
  35. package/dist/chunk-p3pj1bax.js +206 -0
  36. package/dist/chunk-p3pj1bax.js.map +10 -0
  37. package/dist/chunk-vshdk2pw.js +56 -0
  38. package/dist/chunk-vshdk2pw.js.map +10 -0
  39. package/dist/chunk-wp2ebn2m.js +51 -0
  40. package/dist/chunk-wp2ebn2m.js.map +10 -0
  41. package/dist/chunk-x4qv4jm9.js +88 -0
  42. package/dist/chunk-x4qv4jm9.js.map +10 -0
  43. package/dist/chunk-ycxp4drd.js +211 -0
  44. package/dist/chunk-ycxp4drd.js.map +10 -0
  45. package/dist/components/builtin.js +14 -1246
  46. package/dist/components/builtin.js.map +3 -10
  47. package/dist/context.js +7 -362
  48. package/dist/context.js.map +3 -6
  49. package/dist/datasource.js +5 -76
  50. package/dist/datasource.js.map +3 -4
  51. package/dist/disposable.js +20 -367
  52. package/dist/disposable.js.map +3 -5
  53. package/dist/engine-base.d.ts +38 -2
  54. package/dist/engine-base.js +45 -412
  55. package/dist/engine-base.js.map +4 -6
  56. package/dist/events.js +7 -285
  57. package/dist/events.js.map +3 -5
  58. package/dist/hypen.js +8 -71
  59. package/dist/hypen.js.map +3 -4
  60. package/dist/index.browser.js +58 -2152
  61. package/dist/index.browser.js.map +3 -16
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +175 -3057
  64. package/dist/index.js.map +5 -22
  65. package/dist/logger.js +16 -220
  66. package/dist/logger.js.map +3 -4
  67. package/dist/managed-router.js +12 -1367
  68. package/dist/managed-router.js.map +3 -10
  69. package/dist/portable.js +6 -70
  70. package/dist/portable.js.map +3 -4
  71. package/dist/remote/client.js +9 -1010
  72. package/dist/remote/client.js.map +3 -8
  73. package/dist/remote/index.d.ts +2 -0
  74. package/dist/remote/index.js +511 -1066
  75. package/dist/remote/index.js.map +4 -9
  76. package/dist/remote/remote-session.d.ts +245 -0
  77. package/dist/remote/session.js +5 -147
  78. package/dist/remote/session.js.map +3 -4
  79. package/dist/renderer.js +7 -271
  80. package/dist/renderer.js.map +3 -5
  81. package/dist/resolver.js +5 -484
  82. package/dist/resolver.js.map +3 -5
  83. package/dist/result.js +25 -211
  84. package/dist/result.js.map +3 -4
  85. package/dist/retry.js +10 -336
  86. package/dist/retry.js.map +3 -5
  87. package/dist/router.js +9 -807
  88. package/dist/router.js.map +3 -8
  89. package/dist/state.js +10 -274
  90. package/dist/state.js.map +3 -5
  91. package/dist/types.d.ts +129 -1
  92. package/dist/validate.d.ts +19 -0
  93. package/package.json +13 -1
  94. package/src/a11y.ts +127 -0
  95. package/src/engine-base.ts +87 -4
  96. package/src/index.ts +10 -0
  97. package/src/remote/index.ts +16 -0
  98. package/src/remote/remote-session.ts +998 -0
  99. package/src/types.ts +131 -1
  100. package/src/validate.ts +37 -0
package/dist/types.d.ts CHANGED
@@ -16,10 +16,21 @@ export type Patch = {
16
16
  * rebuild. If `remove` arrives for a detached id instead, the
17
17
  * subtree is torn down normally.
18
18
  */
19
- type: "create" | "setProp" | "removeProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent" | "detach" | "attach";
19
+ type: "create" | "setProp" | "removeProp" | "setText" | "insert" | "move" | "remove" | "attachEvent" | "detachEvent" | "detach" | "attach" | "setSemantics";
20
20
  id?: string;
21
21
  elementType?: string;
22
22
  props?: Record<string, any>;
23
+ /**
24
+ * Accessibility semantics derived by the engine for this node. Carried on
25
+ * `create` (first paint) and on `setSemantics` (reactive change — a
26
+ * templated accessible name or bound state whose source path changed).
27
+ * A `setSemantics` always carries the node's **complete** resolved block;
28
+ * renderers re-apply it with the same translation they run at create,
29
+ * clearing anything the new block no longer sets. Absent on `create` when
30
+ * the node has nothing derivable; absent on `setSemantics` when the node
31
+ * lost all semantics (→ clear everything).
32
+ */
33
+ semantics?: Semantics;
23
34
  name?: string;
24
35
  value?: any;
25
36
  text?: string;
@@ -27,6 +38,123 @@ export type Patch = {
27
38
  beforeId?: string;
28
39
  eventName?: string;
29
40
  };
41
+ /**
42
+ * Platform-neutral accessibility semantics for a node, derived in the engine
43
+ * and carried on `create` patches. Each renderer maps this onto its native
44
+ * accessibility model (ARIA attributes on DOM, `Modifier.semantics` on
45
+ * Compose, accessibility traits on SwiftUI).
46
+ *
47
+ * Every field is optional; an absent field means "nothing derivable".
48
+ */
49
+ export type Semantics = {
50
+ /**
51
+ * What this node is, as a platform-neutral role token (e.g. `"button"`).
52
+ * Mirrors the ARIA role vocabulary. Emitted only where the role is
53
+ * structurally certain from the component type.
54
+ */
55
+ role?: string;
56
+ /** Heading level (1–6) when known. */
57
+ level?: number;
58
+ /** Whether the node represents in-progress content (maps to `aria-busy`). */
59
+ busy?: boolean;
60
+ /**
61
+ * Live-region politeness (`.liveRegion("polite" | "assertive")`) →
62
+ * `aria-live` on DOM and the Canvas shadow tree. The engine validates the
63
+ * token, so only those two values arrive. Travels untranslated to the
64
+ * native renderers (no direct analog wired yet).
65
+ */
66
+ live?: string;
67
+ /**
68
+ * Accessible name derived from the element's text content, for roles that
69
+ * require one. On DOM this is usually left unapplied — the browser derives
70
+ * the name from visible content — but renderers without that affordance
71
+ * (Canvas shadow tree, iOS, Android) use it directly.
72
+ */
73
+ name?: string;
74
+ /**
75
+ * `true` when the element's role requires a name but none could be derived
76
+ * (e.g. an icon-only button). Surfaced by dev-mode conformance checks; the
77
+ * fix is an explicit label.
78
+ */
79
+ nameMissing?: boolean;
80
+ /**
81
+ * `true` when {@link name} came from an explicit author `.label(...)` rather
82
+ * than derived content. DOM applies an explicit name as `aria-label` (an
83
+ * intentional override) but leaves a derived name to the visible content.
84
+ */
85
+ nameExplicit?: boolean;
86
+ /**
87
+ * `true` when the element was marked decorative via `.hidden()`. It is
88
+ * removed from the accessibility tree (`aria-hidden`).
89
+ */
90
+ hidden?: boolean;
91
+ /**
92
+ * Supplementary description (`.description(...)`) — extra context beyond the
93
+ * name. Applied as `aria-description` on DOM.
94
+ */
95
+ description?: string;
96
+ /** Disclosure state → `aria-expanded`. */
97
+ expanded?: boolean;
98
+ /** Toggle-button state → `aria-pressed`. */
99
+ pressed?: boolean;
100
+ /** Selected state (e.g. a tab) → `aria-selected`. */
101
+ selected?: boolean;
102
+ /** Current-item token (page/step/…) → `aria-current`. */
103
+ current?: string;
104
+ /**
105
+ * Checked state for a `Checkbox`/`Switch` bound via `.bind(@state.x)` →
106
+ * `aria-checked`. Resolved at reconcile from the control's bind-target value.
107
+ */
108
+ checked?: boolean;
109
+ /**
110
+ * Validity state for a form control (`.invalid(bool | @state.hasError)`) →
111
+ * `aria-invalid`. A bound value resolves at reconcile and stays live via
112
+ * `setSemantics` re-emits. DOM only for now.
113
+ */
114
+ invalid?: boolean;
115
+ /**
116
+ * Cross-node relationship: id of the element this node controls →
117
+ * `aria-controls`. Web-leaning (DOM only); id references do not survive to
118
+ * the string-hint native APIs. See the guide's "Platform support" section
119
+ * (`hypen-docs/content/docs/guide/accessibility.mdx`).
120
+ */
121
+ controls?: string;
122
+ /**
123
+ * Cross-node relationship: id of the element that describes this node →
124
+ * `aria-describedby`. Web-leaning (DOM only), as with `controls`.
125
+ */
126
+ describedby?: string;
127
+ /**
128
+ * The author-supplied stable id of this node (`.id("details-panel")`) —
129
+ * the anchor id-reference relationships resolve against. Applied as the
130
+ * DOM `id` attribute.
131
+ */
132
+ id?: string;
133
+ /**
134
+ * Cross-node relationship: id of the element that labels this node →
135
+ * `aria-labelledby` (the tabpanel→tab half of the Tabs pattern). DOM only.
136
+ */
137
+ labelledby?: string;
138
+ /**
139
+ * Reactive cross-node reference: id of the currently-active descendant →
140
+ * `aria-activedescendant` (the roving focus pointer of composite widgets).
141
+ * Usually bound (`.activedescendant(@state.focusedId)`); kept live via
142
+ * `setSemantics` re-emits. DOM only.
143
+ */
144
+ activeDescendant?: string;
145
+ /**
146
+ * Cross-node relationship: id of an element that is logically this node's
147
+ * child but rendered elsewhere (a portaled popup) → `aria-owns`. DOM only.
148
+ */
149
+ owns?: string;
150
+ /**
151
+ * Base text direction (`.dir("rtl" | "ltr" | "auto")`) → the HTML `dir`
152
+ * attribute on DOM. The engine validates the token, so only those three
153
+ * values arrive. Native renderers translate to their layout-direction
154
+ * APIs (follow-up; the value already travels on the block).
155
+ */
156
+ dir?: string;
157
+ };
30
158
  export type Action = {
31
159
  name: string;
32
160
  payload?: any;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Patch-stream validation. A `create` patch whose `elementType` is neither a
3
+ * known primitive nor a resolved component is a silent resolver miss — the
4
+ * renderer drops the node, no error surfaces. `validatePatches` turns that into
5
+ * a one-line test/CI assertion.
6
+ */
7
+ import type { Patch } from "./types.js";
8
+ export interface PatchValidationResult {
9
+ /** Distinct `create` `elementType`s not in `knownTypes` (i.e. dropped nodes). */
10
+ unknownTypes: string[];
11
+ }
12
+ /**
13
+ * Distinct `create` `elementType`s not in `knownTypes`.
14
+ *
15
+ * `knownTypes` is caller-supplied (engine primitives + the app's resolvable
16
+ * component names) rather than read from the engine: the wasm-bindgen build
17
+ * exposes no primitive list, and hardcoding the built-ins would drift from Rust.
18
+ */
19
+ export declare function validatePatches(patches: Patch[], knownTypes: Iterable<string>): PatchValidationResult;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/core",
3
- "version": "0.4.980",
3
+ "version": "0.5.3",
4
4
  "description": "Hypen core - Platform-agnostic reactive UI runtime (types, state, router, events)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,6 +39,12 @@
39
39
  "import": "./dist/router.js",
40
40
  "default": "./dist/router.js"
41
41
  },
42
+ "./managed-router": {
43
+ "types": "./dist/managed-router.d.ts",
44
+ "bun": "./src/managed-router.ts",
45
+ "import": "./dist/managed-router.js",
46
+ "default": "./dist/managed-router.js"
47
+ },
42
48
  "./events": {
43
49
  "types": "./dist/events.d.ts",
44
50
  "bun": "./src/events.ts",
@@ -106,6 +112,12 @@
106
112
  "import": "./dist/engine-base.js",
107
113
  "default": "./dist/engine-base.js"
108
114
  },
115
+ "./persistence": {
116
+ "types": "./dist/persistence.d.ts",
117
+ "bun": "./src/persistence.ts",
118
+ "import": "./dist/persistence.js",
119
+ "default": "./dist/persistence.js"
120
+ },
109
121
  "./portable": {
110
122
  "types": "./dist/portable.d.ts",
111
123
  "bun": "./src/portable.ts",
package/src/a11y.ts ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Accessibility conformance diagnostics — host-side surfacing.
3
+ *
4
+ * The engine's dev-mode conformance pass (`checkAccessibility(source)` on the
5
+ * WASM engine, wired through the package `Engine` wrappers) returns findings
6
+ * as `A11yDiagnostic[]`. This module provides a small, pure helper for
7
+ * surfacing those findings in a dev console.
8
+ */
9
+
10
+ /**
11
+ * Byte range `[start, end)` of an element's name token in its source string.
12
+ * Mirrors the Rust `SourceSpan` serialization.
13
+ */
14
+ export interface A11ySourceSpan {
15
+ start: number;
16
+ end: number;
17
+ }
18
+
19
+ /**
20
+ * A single accessibility conformance finding produced by the engine.
21
+ *
22
+ * Shape mirrors the Rust located-diagnostic serialization
23
+ * (`{ rule, elementType, message, span?, line?, col? }`). The location
24
+ * fields are present when the engine binding ships span support and the
25
+ * finding maps to a source element; older bindings omit them.
26
+ */
27
+ export interface A11yDiagnostic {
28
+ /** Kebab-case rule identifier (e.g. `"icon-only-control"`). */
29
+ rule: string;
30
+ /** The element type the finding applies to (e.g. `"Button"`). */
31
+ elementType: string;
32
+ /** Human-readable description of the gap. */
33
+ message: string;
34
+ /** Byte span of the offending element's name token in the source. */
35
+ span?: A11ySourceSpan;
36
+ /** 1-based source line of the finding. */
37
+ line?: number;
38
+ /** 1-based column in Unicode codepoints (human/CLI convention). */
39
+ col?: number;
40
+ }
41
+
42
+ /**
43
+ * `console.warn` each accessibility finding as
44
+ * `a11y[<rule>] <elementType>: <message>`.
45
+ *
46
+ * Pure with respect to its input (it mutates nothing and returns the
47
+ * formatted lines), so it is unit-testable: the returned array is exactly
48
+ * what was warned, in order. A `console`-less environment is tolerated — the
49
+ * lines are still returned.
50
+ */
51
+ export function logA11yDiagnostics(diags: A11yDiagnostic[]): string[] {
52
+ const lines = diags.map(
53
+ (d) => `a11y[${d.rule}] ${d.elementType}: ${d.message}`,
54
+ );
55
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
56
+ for (const line of lines) {
57
+ console.warn(line);
58
+ }
59
+ }
60
+ return lines;
61
+ }
62
+
63
+ /**
64
+ * Kebab-case ids of every accessibility rule this SDK version expects the
65
+ * engine's conformance pass to implement. Mirrors the engine's `A11yRule`
66
+ * enum via `ALL_RULES` in `hypen-engine-rs/src/ir/conformance.rs` (the Rust
67
+ * side pins these exact strings in a unit test).
68
+ *
69
+ * Used for rule-set drift detection: a prebuilt WASM that predates a rule
70
+ * still exposes `checkAccessibility` and looks current while silently never
71
+ * firing the newer rule.
72
+ */
73
+ export const EXPECTED_A11Y_RULES: readonly string[] = [
74
+ "missing-accessible-name",
75
+ "image-missing-alt",
76
+ "heading-missing-level",
77
+ "nested-interactive",
78
+ "form-control-missing-label",
79
+ "unknown-role-token",
80
+ "unknown-dir-token",
81
+ "dangling-reference",
82
+ "duplicate-id",
83
+ "tablist-wiring-skipped",
84
+ "non-portable-aria",
85
+ "unknown-live-token",
86
+ "unknown-ignore-rule",
87
+ ];
88
+
89
+ /** Result of {@link checkRuleDrift}. */
90
+ export interface A11yRuleDrift {
91
+ /**
92
+ * Whether the binding exposes `a11yRules()` at all. `false` means the
93
+ * build predates rule-list stamping entirely (every expected rule is
94
+ * reported missing).
95
+ */
96
+ hasRuleList: boolean;
97
+ /** Expected rule ids the binding does not implement; empty means no drift. */
98
+ missing: string[];
99
+ }
100
+
101
+ /**
102
+ * Compare an engine binding's advertised accessibility rules against
103
+ * {@link EXPECTED_A11Y_RULES}. `engine` is anything exposing the WASM
104
+ * binding surface (a raw `WasmEngine` or a wrapper forwarding `a11yRules`).
105
+ *
106
+ * Drift is a warning, not a failure: findings from the rules the binding
107
+ * does implement remain valid, so callers should surface the missing rules
108
+ * and continue rather than abort the check.
109
+ */
110
+ export function checkRuleDrift(
111
+ engine: { a11yRules?: () => string[] } | null | undefined,
112
+ ): A11yRuleDrift {
113
+ if (typeof engine?.a11yRules !== "function") {
114
+ return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };
115
+ }
116
+ let advertised: string[];
117
+ try {
118
+ advertised = engine.a11yRules();
119
+ } catch {
120
+ return { hasRuleList: false, missing: [...EXPECTED_A11Y_RULES] };
121
+ }
122
+ const have = new Set(advertised);
123
+ return {
124
+ hasRuleList: true,
125
+ missing: EXPECTED_A11Y_RULES.filter((rule) => !have.has(rule)),
126
+ };
127
+ }
@@ -46,6 +46,35 @@ import type {
46
46
 
47
47
  const log = frameworkLoggers.engine;
48
48
 
49
+ /**
50
+ * Recursively convert wasm-bindgen `Map` values into plain objects.
51
+ *
52
+ * The web-target WASM returns action payloads (and nested values) as `Map`
53
+ * instances; user handlers expect plain objects. Only plain objects
54
+ * (`constructor === Object`), arrays, and Maps are descended into — class
55
+ * instances, `Date`, etc. are passed through untouched.
56
+ */
57
+ function mapToPlainObject(value: any): any {
58
+ if (value instanceof Map) {
59
+ const obj: Record<string, any> = {};
60
+ for (const [key, val] of value.entries()) {
61
+ obj[key] = mapToPlainObject(val);
62
+ }
63
+ return obj;
64
+ }
65
+ if (Array.isArray(value)) {
66
+ return value.map(mapToPlainObject);
67
+ }
68
+ if (value && typeof value === "object" && value.constructor === Object) {
69
+ const obj: Record<string, any> = {};
70
+ for (const [key, val] of Object.entries(value)) {
71
+ obj[key] = mapToPlainObject(val);
72
+ }
73
+ return obj;
74
+ }
75
+ return value;
76
+ }
77
+
49
78
  /**
50
79
  * Shared base wrapping a wasm-bindgen `WasmEngine`. `any` here is load-
51
80
  * bearing: the concrete type is `WasmEngine` from either the `wasm-node`
@@ -57,6 +86,16 @@ export abstract class BaseEngine {
57
86
  protected wasmEngine: any = null;
58
87
  protected initialized = false;
59
88
 
89
+ /**
90
+ * Component names the resolver was asked for and returned `null` on — i.e.
91
+ * names that are neither a registered primitive (the engine checks those
92
+ * first and never consults the resolver for them) nor resolvable to a
93
+ * template. Each such name produces an opaque `Create` patch the renderer
94
+ * drops. Tracked so a miss is warned about once and can be asserted on in
95
+ * tests. See `getUnresolvedComponents`.
96
+ */
97
+ private unresolvedComponents = new Set<string>();
98
+
60
99
  /**
61
100
  * Initialize the WASM module. Platform-specific.
62
101
  *
@@ -98,16 +137,47 @@ export abstract class BaseEngine {
98
137
 
99
138
  /**
100
139
  * Set the component resolver for dynamic component composition.
140
+ *
141
+ * The resolver is wrapped to detect "resolver misses": the engine only
142
+ * consults the resolver for element types that are NOT registered
143
+ * primitives, so a `null` return means the name resolves to nothing and
144
+ * the engine will emit an opaque `Create` the renderer silently drops
145
+ * (e.g. an anonymous component that never got registered). The first miss
146
+ * for each unique name is logged with actionable guidance; every miss is
147
+ * recorded for `getUnresolvedComponents()` / test assertions.
101
148
  */
102
149
  setComponentResolver(resolver: ComponentResolver): void {
103
150
  const engine = this.ensureInitialized();
104
151
  engine.setComponentResolver(
105
152
  (componentName: string, contextPath: string | null) => {
106
- return resolver(componentName, contextPath);
153
+ const resolved = resolver(componentName, contextPath);
154
+ if (resolved == null) {
155
+ if (!this.unresolvedComponents.has(componentName)) {
156
+ this.unresolvedComponents.add(componentName);
157
+ log.warn(
158
+ `Component "${componentName}" did not resolve: it is not a ` +
159
+ `registered primitive and the component resolver returned null. ` +
160
+ `The engine will emit an opaque Create for it and the renderer ` +
161
+ `will drop it. Did you forget to register/.module(...) the ` +
162
+ `component, or is its name misspelled in the template?`,
163
+ );
164
+ }
165
+ }
166
+ return resolved;
107
167
  },
108
168
  );
109
169
  }
110
170
 
171
+ /**
172
+ * Names the component resolver has been asked for and failed to resolve
173
+ * this session (see `setComponentResolver`). Useful as a CI / smoke-test
174
+ * assertion — an empty array means every referenced component resolved to
175
+ * a primitive or a template.
176
+ */
177
+ getUnresolvedComponents(): string[] {
178
+ return [...this.unresolvedComponents];
179
+ }
180
+
111
181
  /**
112
182
  * Parse and render Hypen DSL source code.
113
183
  * @throws {ParseError} if the source fails to parse
@@ -220,11 +290,24 @@ export abstract class BaseEngine {
220
290
 
221
291
  /**
222
292
  * Platform hook: normalize an incoming action before the user handler
223
- * sees it. Default identity; subclasses can override to unwrap native
224
- * values (e.g. browser Maps in action payloads).
293
+ * sees it.
294
+ *
295
+ * The default converts any `Map` instances in the payload into plain
296
+ * objects (deeply). This is the landmine the wasm-bindgen **web** target
297
+ * sets: it returns structured action payloads as JS `Map`s, so a handler
298
+ * reading `payload.to` gets `undefined` and `@router.push, to: "/x"`
299
+ * silently no-ops. Every consumer of the web-target WASM — the browser
300
+ * engine, a Cloudflare `CFEngine`, any future runtime — inherits the fix
301
+ * here instead of rediscovering it.
302
+ *
303
+ * The wasm-bindgen **bundler** (node) target already returns plain
304
+ * objects, so for it this is a harmless deep walk. Subclasses on that
305
+ * target may override with an identity to skip the walk (see
306
+ * `@hypen-space/server`).
225
307
  */
226
308
  protected normalizeAction(action: Action): Action {
227
- return action;
309
+ if (!action.payload) return action;
310
+ return { ...action, payload: mapToPlainObject(action.payload) };
228
311
  }
229
312
 
230
313
  /**
package/src/index.ts CHANGED
@@ -326,3 +326,13 @@ export {
326
326
  isDebugMode,
327
327
  } from "./logger.js";
328
328
  export type { LogLevel, LoggerConfig, LogHandler } from "./logger.js";
329
+
330
+ // Patch-stream validation — catch silent resolver misses (opaque component
331
+ // elementTypes) in CI / smoke tests off the wire.
332
+ export { validatePatches } from "./validate.js";
333
+ export type { PatchValidationResult } from "./validate.js";
334
+
335
+ // Accessibility conformance diagnostics — surface the engine's dev-mode
336
+ // `checkAccessibility(source)` findings in a host dev console.
337
+ export { logA11yDiagnostics, checkRuleDrift, EXPECTED_A11Y_RULES } from "./a11y.js";
338
+ export type { A11yDiagnostic, A11ySourceSpan, A11yRuleDrift } from "./a11y.js";
@@ -44,3 +44,19 @@ export type {
44
44
 
45
45
  // Re-export Patch type
46
46
  export type { Patch } from "../types.js";
47
+
48
+ // Transport-agnostic remote session. The engine is injected via
49
+ // `SessionHost.createEngine`, so any runtime (Node/Bun, Cloudflare Durable
50
+ // Objects, etc.) can reuse the Hypen remote protocol envelope.
51
+ // `@hypen-space/server` re-exports these for back-compat.
52
+ export {
53
+ RemoteSession,
54
+ AsyncQueueTransport,
55
+ createBunWebSocketTransport,
56
+ } from "./remote-session.js";
57
+ export type {
58
+ SessionTransport,
59
+ SessionHost,
60
+ RemoteSessionOptions,
61
+ OutgoingMessage,
62
+ } from "./remote-session.js";