@opentf/web 0.8.0 → 0.9.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentf/web",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The native-first OTF Web runtime — signal-based reactivity and zero-VDOM DOM operations, paired with the IR-based compiler.",
5
5
  "type": "module",
6
6
  "exports": {
package/runtime/dom.js CHANGED
@@ -147,6 +147,26 @@ export function setAttr(el, name, value) {
147
147
  * component picks it up from `getAttribute` when it upgrades.
148
148
  */
149
149
  export function setProp(el, name, value) {
150
+ // A component prop must land as a *property* (carrying the rich value), never a
151
+ // stringified attribute. A server-parsed custom element that a parent's adopt walk
152
+ // reaches while it is still inert — its own `customElements.define`-driven upgrade
153
+ // hasn't run yet (synchronous upgrades fire in document order, so a parent upgrades
154
+ // and runs this before its later-in-DOM children) — has no accessors, so `name in el`
155
+ // is false. Falling back to `setAttr` would stringify an object to "[object Object]"
156
+ // and, via `attributeChangedCallback`, clobber the child's payload-hydrated signal —
157
+ // flipping conditional branches and desyncing the adopt walk. Force the pending upgrade
158
+ // first so the property setter exists; it's a no-op for already-upgraded or non-custom
159
+ // elements. (A client CSR build creates already-upgraded instances, so this never fires
160
+ // there.)
161
+ if (
162
+ !(name in el) &&
163
+ el.tagName &&
164
+ el.tagName.includes("-") &&
165
+ typeof customElements !== "undefined" &&
166
+ customElements.get(el.tagName.toLowerCase())
167
+ ) {
168
+ customElements.upgrade(el);
169
+ }
150
170
  if (name in el) el[name] = value;
151
171
  else setAttr(el, name, value);
152
172
  }
@@ -58,13 +58,55 @@ export class HydrationMismatch extends Error {
58
58
  // `firstChild`, so the structural test alone can't tell "adopt the server DOM" from
59
59
  // "build and slot these children" (it mis-adopts on plain SPA navigation). The flag is
60
60
  // the unambiguous signal — it is only ever set during the one-shot first-paint pass.
61
- let _hydrating = false;
61
+ //
62
+ // Why it initializes from the DOM sentinel, not `false`: a custom element upgrades — and
63
+ // its `connectedCallback` runs the build-vs-adopt switch — the instant its class is
64
+ // `customElements.define`d. Framework components imported *eagerly* by the app entry
65
+ // (e.g. `<Link>` from `@opentf/web`) are therefore defined and upgraded during the entry
66
+ // bundle's evaluation, **before** `mountApp` runs `beginHydration()`. If the flag were
67
+ // still `false` then, every server-rendered `<web-link>` would take the *build* arm —
68
+ // re-wrapping the server subtree it should have adopted (a silent `<a><a>` double-build)
69
+ // — long before the router ever starts hydrating. So the flag is seeded synchronously
70
+ // at module load from the server sentinel (`[data-otfw-hydrate]`, stamped only when the
71
+ // page shipped adoptable server markup): the deferred entry module evaluates after the
72
+ // HTML is parsed, so the sentinel is present, and this module is a transitive dependency
73
+ // of every component (via the runtime), so it evaluates before any component's `define`.
74
+ // `mountApp` still brackets the pass with `beginHydration`/`endHydration`, and clears the
75
+ // flag when it decides *not* to hydrate (no sentinel / empty root), so a CSR mount and
76
+ // every subsequent SPA navigation build fresh.
77
+ let _hydrating =
78
+ typeof document !== "undefined" && !!document.querySelector("[data-otfw-hydrate]");
62
79
 
63
80
  /** Is the client mid-hydration right now? */
64
81
  export function isHydrating() {
65
82
  return _hydrating;
66
83
  }
67
84
 
85
+ /**
86
+ * Run `fn` with the hydration flag *cleared*, restoring the prior value (nesting-safe).
87
+ *
88
+ * The build/adopt decision hangs on one module-global flag, but the invariant it must
89
+ * encode is narrower: `isHydrating()` may be true only while the DOM being processed is
90
+ * the server's. The moment a component builds fresh DOM — because its view isn't
91
+ * adoptable (`RebuildIfServerChildren`), or its adopt hit a mismatch and it's recovering,
92
+ * or it's a nested build — that fresh subtree is *not* server-rendered. Its child custom
93
+ * elements upgrade synchronously during `appendChild` inside this build, and if they still
94
+ * saw `isHydrating()` true they'd try to *adopt* content their parent just `createElement`'d
95
+ * → a guaranteed `HydrationMismatch` cascade (a non-adoptable layout would tear the whole
96
+ * page's islands into rebuild-storms). Bracketing every build path with this makes the
97
+ * children build too, matching the DOM they're actually handed. Synchronous only — builds
98
+ * never await — so a plain global save/restore is correct even when builds nest.
99
+ */
100
+ export function runBuild(fn) {
101
+ const prev = _hydrating;
102
+ _hydrating = false;
103
+ try {
104
+ return fn();
105
+ } finally {
106
+ _hydrating = prev;
107
+ }
108
+ }
109
+
68
110
  /** Run `fn` with the hydration flag set, restoring the prior value (nesting-safe).
69
111
  * Synchronous only — for the async first-paint pass use {@link beginHydration} /
70
112
  * {@link endHydration}, which span the route module's `import()`. */
package/runtime/router.js CHANGED
@@ -504,6 +504,12 @@ export function mountApp({ pages, target, guard: g, i18n, nav, loaders } = {}) {
504
504
  typeof rootEl.hasAttribute === "function" &&
505
505
  rootEl.hasAttribute("data-otfw-hydrate")
506
506
  );
507
+ // The flag is seeded `true` from the sentinel at module load (so eagerly-defined
508
+ // framework components adopt on upgrade — see hydrate.js). If this mount is NOT
509
+ // hydrating after all (no sentinel, or an empty root), clear it now so the CSR build
510
+ // below — and every later navigation — builds fresh. The hydrate path clears it itself
511
+ // (via `endHydration` in `navigate`'s `finally`) once first paint is adopted.
512
+ if (!hydrate) endHydration();
507
513
  return navigate(
508
514
  window.location.pathname + window.location.search + window.location.hash,
509
515
  true,