@opentf/web 0.5.0 → 0.7.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.
@@ -20,6 +20,53 @@ export function defineSSG(tag, render) {
20
20
  registry[tag] = render;
21
21
  }
22
22
 
23
+ // ── hydration props collector (compiler-driven data hydration) ────────────────
24
+ // The lossy path is passing rich props through string attributes. Instead, each
25
+ // server-rendered island gets a `data-h` id and its JSON-safe props are recorded into a
26
+ // single payload the shell embeds as `<script type="application/json">`. At upgrade the
27
+ // client component initializes its prop *signals* from those real JS values (objects,
28
+ // arrays, numbers) — not from `getAttribute` — so islands resume with correct data, with
29
+ // no flash and no dependence on a parent walk re-applying props. `renderRoute` brackets a
30
+ // render with `beginHydrationCollect()` / `endHydrationCollect()`.
31
+ let _collect = null;
32
+
33
+ /** Start collecting per-island hydration props for one render. */
34
+ export function beginHydrationCollect() {
35
+ _collect = [];
36
+ }
37
+
38
+ /**
39
+ * Finish collecting and return the payload as a JSON string ("" when nothing needs
40
+ * hydration). `<` is escaped so a prop value can never break out of the surrounding
41
+ * `<script>` (a `</script>` / `<!--` injection).
42
+ */
43
+ export function endHydrationCollect() {
44
+ const data = _collect;
45
+ _collect = null;
46
+ if (!data || data.length === 0) return "";
47
+ return JSON.stringify(data).replace(/</g, "\\u003c");
48
+ }
49
+
50
+ /**
51
+ * Record an island's JSON-safe props, returning its hydration id — or `null` when not
52
+ * collecting, or when nothing serializes. A JSON round-trip drops functions (event
53
+ * callbacks are client-only — applied by the parent walk, invisible so no flash),
54
+ * `undefined`, and anything cyclic/DOM/signal-shaped, so only plain data crosses.
55
+ */
56
+ function collectHydrationProps(props) {
57
+ if (!_collect || props == null || typeof props !== "object") return null;
58
+ let safe;
59
+ try {
60
+ safe = JSON.parse(JSON.stringify(props));
61
+ } catch {
62
+ return null; // cyclic / non-serializable → client falls back to attributes/defaults
63
+ }
64
+ if (!safe || typeof safe !== "object" || Object.keys(safe).length === 0) return null;
65
+ const id = _collect.length;
66
+ _collect.push(safe);
67
+ return id;
68
+ }
69
+
23
70
  /** Escape text content for HTML. */
24
71
  export function escapeHtml(s) {
25
72
  return String(s).replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"));
@@ -103,7 +150,14 @@ export function ssgComponent(tag, props, children) {
103
150
  } catch {
104
151
  inner = ""; // fail soft (client renders/handles it)
105
152
  }
106
- return `<${tag}>${inner}</${tag}>`;
153
+ // Stamp the stable styling hook on the host (mirrors CSR's `classList.add`) so
154
+ // tag-hashed components are still styleable by a readable class name, plus the
155
+ // hydration id (`data-h`) keying this island's rich props in the payload (if any).
156
+ const cls = render && render.hostClass;
157
+ const id = collectHydrationProps(props);
158
+ let attrs = cls ? ` class="${cls}"` : "";
159
+ if (id != null) attrs += ` data-h="${id}"`;
160
+ return `<${tag}${attrs}>${inner}</${tag}>`;
107
161
  }
108
162
 
109
163
  export { VOID };